diff --git a/Roslyn.sln b/Roslyn.sln index b54926c007207..848adc0f76206 100644 --- a/Roslyn.sln +++ b/Roslyn.sln @@ -547,8 +547,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Replay", "src\Tools\Replay\ EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "Microsoft.CommonLanguageServerProtocol.Framework.Shared", "src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.shproj", "{64EADED3-4B5D-4431-BBE5-A4ABA1C38C00}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CommonLanguageServerProtocol.Framework.Binary", "src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework.Binary\Microsoft.CommonLanguageServerProtocol.Framework.Binary.csproj", "{730CADBA-701F-4722-9B6F-1FCC0DF2C95D}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.CodeAnalysis.CSharp.Emit3.UnitTests", "src\Compilers\CSharp\Test\Emit3\Microsoft.CodeAnalysis.CSharp.Emit3.UnitTests.csproj", "{4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SemanticSearch", "SemanticSearch", "{52ABB0E4-C3A1-4897-B51B-18EDA83F5D20}" @@ -1365,10 +1363,6 @@ Global {DB96C25F-39A9-4A6A-92BC-D1E42717308F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DB96C25F-39A9-4A6A-92BC-D1E42717308F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB96C25F-39A9-4A6A-92BC-D1E42717308F}.Release|Any CPU.Build.0 = Release|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D}.Release|Any CPU.Build.0 = Release|Any CPU {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1636,7 +1630,6 @@ Global {6D819E80-BA2F-4317-8368-37F8F4434D3A} = {8977A560-45C2-4EC2-A849-97335B382C74} {DB96C25F-39A9-4A6A-92BC-D1E42717308F} = {FD0FAF5F-1DED-485C-99FA-84B97F3A8EEC} {64EADED3-4B5D-4431-BBE5-A4ABA1C38C00} = {D449D505-CC6A-4E0B-AF1B-976E2D0AE67A} - {730CADBA-701F-4722-9B6F-1FCC0DF2C95D} = {D449D505-CC6A-4E0B-AF1B-976E2D0AE67A} {4E273CBC-BB1D-4AC1-91DB-C62FC83E0350} = {32A48625-F0AD-419D-828B-A50BDABA38EA} {52ABB0E4-C3A1-4897-B51B-18EDA83F5D20} = {FD0FAF5F-1DED-485C-99FA-84B97F3A8EEC} {FCE88BBD-9BBD-4871-B9B0-DE176D73A6B0} = {52ABB0E4-C3A1-4897-B51B-18EDA83F5D20} @@ -1686,7 +1679,6 @@ Global src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems*{64eaded3-4b5d-4431-bbe5-a4aba1c38c00}*SharedItemsImports = 13 src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems*{686bf57e-a6ff-467b-aab3-44de916a9772}*SharedItemsImports = 5 src\Workspaces\SharedUtilitiesAndExtensions\Compiler\CSharp\CSharpCompilerExtensions.projitems*{699fea05-aea7-403d-827e-53cf4e826955}*SharedItemsImports = 13 - src\Features\LanguageServer\Microsoft.CommonLanguageServerProtocol.Framework\Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems*{730cadba-701f-4722-9b6f-1fcc0df2c95d}*SharedItemsImports = 5 src\ExpressionEvaluator\VisualBasic\Source\ResultProvider\BasicResultProvider.projitems*{76242a2d-2600-49dd-8c15-fea07ecb1843}*SharedItemsImports = 5 src\Analyzers\Core\Analyzers\Analyzers.projitems*{76e96966-4780-4040-8197-bde2879516f4}*SharedItemsImports = 13 src\Analyzers\VisualBasic\Tests\VisualBasicAnalyzers.UnitTests.projitems*{7b7f4153-ae93-4908-b8f0-430871589f83}*SharedItemsImports = 13 diff --git a/azure-pipelines-official.yml b/azure-pipelines-official.yml index 1a86360a0fcb0..5f681633869f5 100644 --- a/azure-pipelines-official.yml +++ b/azure-pipelines-official.yml @@ -85,7 +85,7 @@ extends: sdl: sourceAnalysisPool: name: NetCore1ESPool-Svc-Internal - image: 1es-windows-2022-pt + image: 1es-windows-2022 os: windows sbom: enabled: false diff --git a/docs/Language Feature Status.md b/docs/Language Feature Status.md index 3ba66cf3fe790..6a36d6df37b25 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 | | ------- | ------ | ----- | --------- | -------- | --------- | --------- | +| [Partial properties](https://github.com/dotnet/csharplang/issues/6420) | [partial-properties](https://github.com/dotnet/roslyn/tree/features/partial-properties) | [In Progress](https://github.com/dotnet/roslyn/issues/73090) | [RikkiGibson](https://github.com/RikkiGibson) | [jcouv](https://github.com/jcouv), [333fred](https://github.com/333fred) | TBD | [333fred](https://github.com/333fred), [RikkiGibson](https://github.com/RikkiGibson) | | Ref/unsafe in iterators/async | [RefInAsync](https://github.com/dotnet/roslyn/tree/features/RefInAsync) | [In Progress](https://github.com/dotnet/roslyn/issues/72662) | [jjonescz](https://github.com/jjonescz) | [AlekseyTs](https://github.com/AlekseyTs), [cston](https://github.com/cston) | [ToddGrun](https://github.com/ToddGrun) | | | [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) | [ToddGrun](https://github.com/ToddGrun) | [agocke](https://github.com/agocke), [jaredpar](https://github.com/jaredpar) | | [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) | diff --git a/docs/contributing/Bootstrap Builds.md b/docs/contributing/Bootstrap Builds.md index 52e1a638cc877..128a4d59fe740 100644 --- a/docs/contributing/Bootstrap Builds.md +++ b/docs/contributing/Bootstrap Builds.md @@ -104,5 +104,5 @@ https://github.com/dotnet/roslyn/blob/d73d31cbccb9aa850f3582afb464b709fef88fd7/s Next just run the bootstrap build locally, wait for the `Debug.Assert` to trigger which pops up a dialog. From there you can attach to the VBCSCompiler process and debug through the problem ```cmd -> Build.cmd -bootstrap -bootstrapConfiguration Debug +> Build.cmd -bootstrap ``` diff --git a/docs/features/incremental-generators.cookbook.md b/docs/features/incremental-generators.cookbook.md index 9bbb0b9134c9d..3cf0870f6bf57 100644 --- a/docs/features/incremental-generators.cookbook.md +++ b/docs/features/incremental-generators.cookbook.md @@ -191,7 +191,7 @@ public class FileTransformGenerator : IIncrementalGenerator public void Initialize(IncrementalGeneratorInitializationContext context) { var pipeline = context.AdditionalTextsProvider - .Where(static (text, cancellationToken) => text.Path.EndsWith(".xml")) + .Where(static (text) => text.Path.EndsWith(".xml")) .Select(static (text, cancellationToken) => { var name = Path.GetFileName(text.Path); diff --git a/eng/Directory.Packages.props b/eng/Directory.Packages.props index 97ff84ec7abe2..d9970c400f1f0 100644 --- a/eng/Directory.Packages.props +++ b/eng/Directory.Packages.props @@ -103,14 +103,14 @@ - + - + diff --git a/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs index 0295d3a9908cc..7072dff8597cf 100644 --- a/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/MisplacedUsingDirectives/MisplacedUsingDirectivesDiagnosticAnalyzer.cs @@ -3,15 +3,12 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis.AddImport; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp.CodeStyle; -using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.CSharp.MisplacedUsingDirectives; @@ -40,9 +37,11 @@ internal sealed class MisplacedUsingDirectivesDiagnosticAnalyzer : AbstractBuilt s_localizableTitle, s_localizableInsideMessage); public MisplacedUsingDirectivesDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_outsideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement) - .Add(s_insideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement)) + : base( + [ + (s_outsideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement), + (s_insideDiagnosticDescriptor, CSharpCodeStyleOptions.PreferredUsingDirectivePlacement) + ]) { } diff --git a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer.cs index 7fe59d662f13d..b1daeecb8190a 100644 --- a/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseCollectionExpression/AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Shared.Extensions; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.UseCollectionInitializer; @@ -22,18 +21,15 @@ internal abstract class AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer public static readonly ImmutableDictionary ChangesSemantics = ImmutableDictionary.Empty.Add(UseCollectionInitializerHelpers.ChangesSemanticsName, ""); - protected new readonly DiagnosticDescriptor Descriptor; protected readonly DiagnosticDescriptor UnnecessaryCodeDescriptor; protected AbstractCSharpUseCollectionExpressionDiagnosticAnalyzer(string diagnosticId, EnforceOnBuild enforceOnBuild) - : base(ImmutableDictionary.Empty - // Ugly hack. We need to create a descriptor to pass to our base *and* assign to one of our fields. - // The conditional pattern form lets us do that. - .Add(CreateDescriptor(diagnosticId, enforceOnBuild, isUnnecessary: false) is var descriptor ? descriptor : null, CodeStyleOptions2.PreferCollectionExpression) - .Add(CreateDescriptor(diagnosticId, enforceOnBuild, isUnnecessary: true) is var unnecessaryCodeDescriptor ? unnecessaryCodeDescriptor : null, CodeStyleOptions2.PreferCollectionExpression)) + : base( + [ + (CreateDescriptor(diagnosticId, enforceOnBuild, isUnnecessary: false), CodeStyleOptions2.PreferCollectionExpression) + ]) { - Descriptor = descriptor; - UnnecessaryCodeDescriptor = unnecessaryCodeDescriptor; + UnnecessaryCodeDescriptor = CreateDescriptor(diagnosticId, enforceOnBuild, isUnnecessary: true); } private static DiagnosticDescriptor CreateDescriptor(string diagnosticId, EnforceOnBuild enforceOnBuild, bool isUnnecessary) diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs index bed23c5b6d1a6..fa00a7e87a56b 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBody/UseExpressionBodyDiagnosticAnalyzer.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp.CodeGeneration; @@ -14,7 +13,7 @@ namespace Microsoft.CodeAnalysis.CSharp.UseExpressionBody; [DiagnosticAnalyzer(LanguageNames.CSharp)] -internal class UseExpressionBodyDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer +internal sealed class UseExpressionBodyDiagnosticAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer { public const string FixesError = nameof(FixesError); @@ -28,16 +27,16 @@ public UseExpressionBodyDiagnosticAnalyzer() _syntaxKinds = _helpers.SelectManyAsArray(h => h.SyntaxKinds); } - private static ImmutableDictionary GetSupportedDescriptorsWithOptions() + private static ImmutableArray<(DiagnosticDescriptor, IOption2)> GetSupportedDescriptorsWithOptions() { - var builder = ImmutableDictionary.CreateBuilder(); + var builder = new FixedSizeArrayBuilder<(DiagnosticDescriptor, IOption2)>(_helpers.Length); foreach (var helper in _helpers) { var descriptor = CreateDescriptorWithId(helper.DiagnosticId, helper.EnforceOnBuild, hasAnyCodeStyleOption: true, helper.UseExpressionBodyTitle, helper.UseExpressionBodyTitle); - builder.Add(descriptor, helper.Option); + builder.Add((descriptor, helper.Option)); } - return builder.ToImmutable(); + return builder.MoveToImmutable(); } public override DiagnosticAnalyzerCategory GetAnalyzerCategory() diff --git a/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs b/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs index d4248991e4b42..21af7ba3f40bd 100644 --- a/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs +++ b/src/Analyzers/CSharp/Analyzers/UseExpressionBodyForLambda/UseExpressionBodyForLambdaDiagnosticAnalyzer.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp.CodeStyle; @@ -21,9 +20,10 @@ internal sealed class UseExpressionBodyForLambdaDiagnosticAnalyzer : AbstractBui 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)) + [ + (s_useExpressionBodyForLambda, CSharpCodeStyleOptions.PreferExpressionBodiedLambdas), + (s_useBlockBodyForLambda, CSharpCodeStyleOptions.PreferExpressionBodiedLambdas) + ]) { } diff --git a/src/Analyzers/CSharp/Tests/AbstractBuiltInCodeStyleDiagnosticAnalyzer/AbstractBuiltInCodeStyleDiagnosticAnalyzerTests.cs b/src/Analyzers/CSharp/Tests/AbstractBuiltInCodeStyleDiagnosticAnalyzer/AbstractBuiltInCodeStyleDiagnosticAnalyzerTests.cs new file mode 100644 index 0000000000000..616ccf33ffd80 --- /dev/null +++ b/src/Analyzers/CSharp/Tests/AbstractBuiltInCodeStyleDiagnosticAnalyzer/AbstractBuiltInCodeStyleDiagnosticAnalyzerTests.cs @@ -0,0 +1,59 @@ +// 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.Editor.CSharp.UnitTests.AbstractBuiltInCodeStyleDiagnosticAnalyzer; + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Options; +using Roslyn.Utilities; +using Xunit; + +public sealed class AbstractBuiltInCodeStyleDiagnosticAnalyzerTests +{ + [Fact] + public void VerifyDiagnosticDescriptorOrderingMaintained() + { + var ids = Enumerable.Range(10, 20).Select(item => "IDE_" + item); + + var analyzer = new TestAnalyzer(ids); + + Assert.Equal(analyzer.SupportedDiagnostics.Select(static diagnostic => diagnostic.Id), ids); + } + + private sealed class TestAnalyzer : AbstractBuiltInCodeStyleDiagnosticAnalyzer + { + public TestAnalyzer(IEnumerable ids) + : base(CreateSupportedDiagnosticsWithOptionsFromIds(ids)) + { + } + + private static ImmutableArray<(DiagnosticDescriptor, ImmutableHashSet)> CreateSupportedDiagnosticsWithOptionsFromIds(IEnumerable ids) + { + var builder = ImmutableArray.CreateBuilder<(DiagnosticDescriptor, ImmutableHashSet)>(); + foreach (var id in ids) + { + var descriptor = CreateDescriptorWithId( + id: id, + enforceOnBuild: EnforceOnBuild.Never, + hasAnyCodeStyleOption: false, + title: string.Empty); + + builder.Add((descriptor, [])); + } + + return builder.ToImmutableAndClear(); + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => throw new System.NotImplementedException(); + + protected override void InitializeWorker(AnalysisContext context) + { + } + } +} diff --git a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems index 1ce11ff7471ba..472b6d7133ff3 100644 --- a/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems +++ b/src/Analyzers/CSharp/Tests/CSharpAnalyzers.UnitTests.projitems @@ -9,6 +9,7 @@ Microsoft.CodeAnalysis.CSharp.Analyzers.UnitTests + @@ -181,4 +182,7 @@ + + + \ No newline at end of file diff --git a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs index 2a9338532b838..9b8b81878119e 100644 --- a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer.cs @@ -4,7 +4,6 @@ using System.Collections.Immutable; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; @@ -81,8 +80,8 @@ protected AbstractBuiltInCodeStyleDiagnosticAnalyzer( /// /// 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()) + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableArray<(DiagnosticDescriptor Descriptor, IOption2 Option)> supportedDiagnosticsWithOptions) + : this(supportedDiagnosticsWithOptions.SelectAsArray(static item => item.Descriptor)) { foreach (var (descriptor, option) in supportedDiagnosticsWithOptions) { @@ -94,8 +93,8 @@ protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableDictionary /// 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()) + protected AbstractBuiltInCodeStyleDiagnosticAnalyzer(ImmutableArray<(DiagnosticDescriptor Descriptor, ImmutableHashSet Options)> supportedDiagnosticsWithOptions) + : this(supportedDiagnosticsWithOptions.SelectAsArray(static item => item.Descriptor)) { foreach (var (descriptor, options) in supportedDiagnosticsWithOptions) { diff --git a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs index 59d80030b5808..b91fd230bf02f 100644 --- a/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs +++ b/src/Analyzers/Core/Analyzers/AbstractBuiltInCodeStyleDiagnosticAnalyzer_Core.cs @@ -2,14 +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. -using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CodeStyle; diff --git a/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs index 4ca8ddd49631f..922dc5ad9ff11 100644 --- a/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer.cs @@ -100,19 +100,19 @@ protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableArray /// 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) + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableArray<(DiagnosticDescriptor Descriptor, IOption2 Option)> supportedDiagnosticsWithOptions, PerLanguageOption2? fadingOption) : base(supportedDiagnosticsWithOptions) { - AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Keys, fadingOption); + AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Select(static item => item.Descriptor), 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) + protected AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer(ImmutableArray<(DiagnosticDescriptor Descriptor, ImmutableHashSet Options)> supportedDiagnosticsWithOptions, PerLanguageOption2? fadingOption) : base(supportedDiagnosticsWithOptions) { - AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Keys, fadingOption); + AddDescriptorsToFadingOptionMapping(supportedDiagnosticsWithOptions.Select(static item => item.Descriptor), fadingOption); } private static void AddDiagnosticIdToFadingOptionMapping(string diagnosticId, PerLanguageOption2? fadingOption) diff --git a/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs index e356bc1c9b206..9f8bfae89f846 100644 --- a/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/FileHeaders/AbstractFileHeaderDiagnosticAnalyzer.cs @@ -2,11 +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. -using System.Collections.Immutable; using System.IO; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.FileHeaders; @@ -24,9 +22,11 @@ private static DiagnosticDescriptor CreateDescriptorForFileHeader(LocalizableStr => CreateDescriptorWithId(IDEDiagnosticIds.FileHeaderMismatch, EnforceOnBuildValues.FileHeaderMismatch, hasAnyCodeStyleOption: true, title, message); protected AbstractFileHeaderDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_invalidHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate) - .Add(s_missingHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate)) + : base( + [ + (s_invalidHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate), + (s_missingHeaderDescriptor, CodeStyleOptions2.FileHeaderTemplate) + ]) { } diff --git a/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs b/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs index 7c3a5d350984d..b657d4cd0f720 100644 --- a/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs +++ b/src/Analyzers/Core/Analyzers/IDEDiagnosticIdToOptionMappingHelper.cs @@ -69,7 +69,7 @@ public static void AddOptionMapping(string diagnosticId, ImmutableHashSet new ConcurrentDictionary>()); - AddOptionMapping(map, diagnosticId, languageGroup.ToImmutableHashSet()); + AddOptionMapping(map, diagnosticId, [.. languageGroup]); } } } diff --git a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs index 690ac016a84b8..09655f4fec19a 100644 --- a/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/RemoveUnnecessaryImports/AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer.cs @@ -40,7 +40,7 @@ internal abstract class AbstractRemoveUnnecessaryImportsDiagnosticAnalyzer> unusedValueExpressionStatementOption, Option2> unusedValueAssignmentOption) - : base(ImmutableDictionary.Empty - .Add(s_expressionValueIsUnusedRule, unusedValueExpressionStatementOption) - .Add(s_valueAssignedIsUnusedRule, unusedValueAssignmentOption) - .Add(s_unusedParameterRule, CodeStyleOptions2.UnusedParameters), - fadingOption: null) + : base( + [ + (s_expressionValueIsUnusedRule, unusedValueExpressionStatementOption), + (s_valueAssignedIsUnusedRule, unusedValueAssignmentOption), + (s_unusedParameterRule, CodeStyleOptions2.UnusedParameters) + ], + fadingOption: null) { } diff --git a/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs b/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs index a37aa66a8d07f..fdaf88e7069f3 100644 --- a/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs +++ b/src/Analyzers/Core/Analyzers/SimplifyTypeNames/SimplifyTypeNamesDiagnosticAnalyzerBase.cs @@ -4,19 +4,16 @@ // #define LOG -using System; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -64,15 +61,17 @@ internal abstract class SimplifyTypeNamesDiagnosticAnalyzerBase>.Empty - .Add(s_descriptorSimplifyNames, []) - .Add(s_descriptorSimplifyMemberAccess, []) - .Add(s_descriptorPreferBuiltinOrFrameworkType, - [ - CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration, - CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess, - ]), - fadingOption: null) + : base( + [ + (s_descriptorSimplifyNames, []), + (s_descriptorSimplifyMemberAccess, []), + (s_descriptorPreferBuiltinOrFrameworkType, + [ + CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInDeclaration, + CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess, + ]) + ], + fadingOption: null) { } diff --git a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs index ea645fb31ee2a..a663612d196d2 100644 --- a/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseCollectionInitializer/AbstractUseCollectionInitializerDiagnosticAnalyzer.cs @@ -8,7 +8,6 @@ using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.CodeStyle; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -82,9 +81,10 @@ public override DiagnosticAnalyzerCategory GetAnalyzerCategory() isUnnecessary: true); protected AbstractUseCollectionInitializerDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_descriptor, CodeStyleOptions2.PreferCollectionInitializer) - .Add(s_unnecessaryCodeDescriptor, CodeStyleOptions2.PreferCollectionInitializer)) + : base( + [ + (s_descriptor, CodeStyleOptions2.PreferCollectionInitializer) + ]) { } diff --git a/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs b/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs index 8e1c39d240270..aabbb18b1eb49 100644 --- a/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs +++ b/src/Analyzers/Core/Analyzers/UseObjectInitializer/AbstractUseObjectInitializerDiagnosticAnalyzer.cs @@ -7,7 +7,6 @@ using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Text; @@ -62,9 +61,10 @@ internal abstract partial class AbstractUseObjectInitializerDiagnosticAnalyzer< protected abstract TAnalyzer GetAnalyzer(); protected AbstractUseObjectInitializerDiagnosticAnalyzer() - : base(ImmutableDictionary.Empty - .Add(s_descriptor, CodeStyleOptions2.PreferObjectInitializer) - .Add(s_unnecessaryCodeDescriptor, CodeStyleOptions2.PreferObjectInitializer)) + : base( + [ + (s_descriptor, CodeStyleOptions2.PreferObjectInitializer) + ]) { } diff --git a/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs b/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs index 2029d8d5aa669..04ac043860e7d 100644 --- a/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs +++ b/src/Compilers/CSharp/Portable/Binder/RefSafetyAnalysis.cs @@ -258,6 +258,24 @@ private bool ContainsPlaceholderScope(BoundValuePlaceholderBase placeholder) public override BoundNode? Visit(BoundNode? node) { #if DEBUG + TrackVisit(node); +#endif + return base.Visit(node); + } + +#if DEBUG + protected override void BeforeVisitingSkippedBoundBinaryOperatorChildren(BoundBinaryOperator node) + { + TrackVisit(node); + } + + protected override void BeforeVisitingSkippedBoundCallChildren(BoundCall node) + { + TrackVisit(node); + } + + private void TrackVisit(BoundNode? node) + { if (node is BoundValuePlaceholderBase placeholder) { Debug.Assert(ContainsPlaceholderScope(placeholder)); @@ -267,14 +285,11 @@ private bool ContainsPlaceholderScope(BoundValuePlaceholderBase placeholder) if (_visited is { } && _visited.Count <= MaxTrackVisited) { bool added = _visited.Add(expr); - Debug.Assert(added); + Debug.Assert(added, $"Expression {expr} `{expr.Syntax}` visited more than once."); } } -#endif - return base.Visit(node); } -#if DEBUG private void AssertVisited(BoundExpression expr) { if (expr is BoundValuePlaceholderBase placeholder) @@ -283,7 +298,7 @@ private void AssertVisited(BoundExpression expr) } else if (_visited is { } && _visited.Count <= MaxTrackVisited) { - Debug.Assert(_visited.Contains(expr)); + Debug.Assert(_visited.Contains(expr), $"Expected {expr} `{expr.Syntax}` to be visited."); } } #endif @@ -659,13 +674,6 @@ protected override void VisitArguments(BoundCall node) _localScopeDepth, _diagnostics); } - -#if DEBUG - if (_visited is { } && _visited.Count <= MaxTrackVisited) - { - _visited.Add(node); - } -#endif } private void GetInterpolatedStringPlaceholders( diff --git a/src/Compilers/CSharp/Portable/BoundTree/BoundTreeWalker.cs b/src/Compilers/CSharp/Portable/BoundTree/BoundTreeWalker.cs index a8ed41a6f003c..8f0df23b1e56c 100644 --- a/src/Compilers/CSharp/Portable/BoundTree/BoundTreeWalker.cs +++ b/src/Compilers/CSharp/Portable/BoundTree/BoundTreeWalker.cs @@ -114,6 +114,7 @@ protected BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator var binary = (BoundBinaryOperator)node.Left; + BeforeVisitingSkippedBoundBinaryOperatorChildren(binary); rightOperands.Push(binary.Right); BoundExpression current = binary.Left; @@ -121,6 +122,7 @@ protected BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator while (current.Kind == BoundKind.BinaryOperator) { binary = (BoundBinaryOperator)current; + BeforeVisitingSkippedBoundBinaryOperatorChildren(binary); rightOperands.Push(binary.Right); current = binary.Left; } @@ -136,6 +138,10 @@ protected BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator return null; } + protected virtual void BeforeVisitingSkippedBoundBinaryOperatorChildren(BoundBinaryOperator node) + { + } + public sealed override BoundNode? VisitCall(BoundCall node) { if (node.ReceiverOpt is BoundCall receiver1) @@ -147,10 +153,13 @@ protected BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator node = receiver1; while (node.ReceiverOpt is BoundCall receiver2) { + BeforeVisitingSkippedBoundCallChildren(node); calls.Push(node); node = receiver2; } + BeforeVisitingSkippedBoundCallChildren(node); + VisitReceiver(node); do @@ -170,6 +179,10 @@ protected BoundTreeWalkerWithStackGuardWithoutRecursionOnTheLeftOfBinaryOperator return null; } + protected virtual void BeforeVisitingSkippedBoundCallChildren(BoundCall node) + { + } + /// /// Called only for the first (in evaluation order) in the chain. /// diff --git a/src/Compilers/CSharp/Portable/Parser/Blender.Cursor.cs b/src/Compilers/CSharp/Portable/Parser/Blender.Cursor.cs index 4f9b6796441b9..cab4183fbee13 100644 --- a/src/Compilers/CSharp/Portable/Parser/Blender.Cursor.cs +++ b/src/Compilers/CSharp/Portable/Parser/Blender.Cursor.cs @@ -55,7 +55,11 @@ private static bool IsNonZeroWidthOrIsEndOfFile(SyntaxNodeOrToken token) return token.Kind() == SyntaxKind.EndOfFileToken || token.FullWidth != 0; } - public Cursor MoveToNextSibling() + /// + /// Returns the cursor of our next non-empty (or EOF) sibling in our parent if one exists, or `default` if + /// if doesn't. + /// + private Cursor TryFindNextNonZeroWidthOrIsEndOfFileSibling() { if (this.CurrentNodeOrToken.Parent != null) { @@ -70,10 +74,6 @@ public Cursor MoveToNextSibling() return new Cursor(sibling, i); } } - - // We're at the end of this sibling chain. Walk up to the parent and see who is - // the next sibling of that. - return MoveToParent().MoveToNextSibling(); } return default(Cursor); @@ -86,6 +86,26 @@ private Cursor MoveToParent() return new Cursor(parent, index); } + public static Cursor MoveToNextSibling(Cursor cursor) + { + // Iteratively walk over the tree so that we don't stack overflow trying to recurse into anything. + while (cursor.CurrentNodeOrToken.UnderlyingNode != null) + { + var nextSibling = cursor.TryFindNextNonZeroWidthOrIsEndOfFileSibling(); + + // If we got a valid sibling, return it. + if (nextSibling.CurrentNodeOrToken.UnderlyingNode != null) + return nextSibling; + + // We're at the end of this sibling chain. Walk up to the parent and see who is + // the next sibling of that. + cursor = cursor.MoveToParent(); + } + + // Couldn't find anything, bail out. + return default; + } + private static int IndexOfNodeInParent(SyntaxNode node) { if (node.Parent == null) diff --git a/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs b/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs index f0e6877589899..4181b444e1c75 100644 --- a/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs +++ b/src/Compilers/CSharp/Portable/Parser/Blender.Reader.cs @@ -118,7 +118,7 @@ private void SkipOldToken() // Now, skip past it. _changeDelta += node.FullWidth; _oldDirectives = node.ApplyDirectives(_oldDirectives); - _oldTreeCursor = _oldTreeCursor.MoveToNextSibling(); + _oldTreeCursor = Cursor.MoveToNextSibling(_oldTreeCursor); // If our cursor is now after any changes, then just skip past them while upping // the changeDelta length. This will let us know that we need to read tokens @@ -204,7 +204,7 @@ private bool TryTakeOldNodeOrToken( // We can reuse this node or token. Move us forward in the new text, and move to the // next sibling. _newPosition += currentNodeOrToken.FullWidth; - _oldTreeCursor = _oldTreeCursor.MoveToNextSibling(); + _oldTreeCursor = Cursor.MoveToNextSibling(_oldTreeCursor); _newDirectives = currentNodeOrToken.ApplyDirectives(_newDirectives); _oldDirectives = currentNodeOrToken.ApplyDirectives(_oldDirectives); diff --git a/src/Compilers/CSharp/Portable/Syntax/SyntaxEquivalence.cs b/src/Compilers/CSharp/Portable/Syntax/SyntaxEquivalence.cs index 6484b75a769c9..cba16e079df44 100644 --- a/src/Compilers/CSharp/Portable/Syntax/SyntaxEquivalence.cs +++ b/src/Compilers/CSharp/Portable/Syntax/SyntaxEquivalence.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; namespace Microsoft.CodeAnalysis.CSharp.Syntax { @@ -100,130 +101,147 @@ private static bool AreTokensEquivalent(GreenNode? before, GreenNode? after, Fun private static bool AreEquivalentRecursive(GreenNode? before, GreenNode? after, Func? ignoreChildNode, bool topLevel) { - if (before == after) - { - return true; - } + // Use an explicit stack so we can walk down deep trees without blowing the real stack. + var stack = ArrayBuilder<(GreenNode? before, GreenNode? after)>.GetInstance(); + stack.Push((before, after)); - if (before == null || after == null) + try { - return false; - } + while (stack.TryPop(out var current)) + { + if (!areEquivalentSingleLevel(current.before, current.after)) + return false; + } - if (before.RawKind != after.RawKind) - { - return false; + return true; } - - if (before.IsToken) + finally { - Debug.Assert(after.IsToken); - return AreTokensEquivalent(before, after, ignoreChildNode); + stack.Free(); } - if (topLevel) + bool areEquivalentSingleLevel(GreenNode? before, GreenNode? after) { - // Once we get down to the body level we don't need to go any further and we can - // consider these trees equivalent. - switch ((SyntaxKind)before.RawKind) + if (before == after) { - case SyntaxKind.Block: - case SyntaxKind.ArrowExpressionClause: - return AreNullableDirectivesEquivalent(before, after, ignoreChildNode); + return true; } - // If we're only checking top level equivalence, then we don't have to go down into - // the initializer for a field. However, we can't put that optimization for all - // fields. For example, fields that are 'const' do need their initializers checked as - // changing them can affect binding results. - if ((SyntaxKind)before.RawKind == SyntaxKind.FieldDeclaration) + if (before == null || after == null) { - var fieldBefore = (Green.FieldDeclarationSyntax)before; - var fieldAfter = (Green.FieldDeclarationSyntax)after; - - var isConstBefore = fieldBefore.Modifiers.Any((int)SyntaxKind.ConstKeyword); - var isConstAfter = fieldAfter.Modifiers.Any((int)SyntaxKind.ConstKeyword); + return false; + } - if (!isConstBefore && !isConstAfter) - { - ignoreChildNode = childKind => childKind == SyntaxKind.EqualsValueClause; - } + if (before.RawKind != after.RawKind) + { + return false; } - // NOTE(cyrusn): Do we want to avoid going down into attribute expressions? I don't - // think we can avoid it as there are likely places in the compiler that use these - // expressions. For example, if the user changes [InternalsVisibleTo("goo")] to - // [InternalsVisibleTo("bar")] then that must count as a top level change as it - // affects symbol visibility. Perhaps we could enumerate the places in the compiler - // that use the values inside source attributes and we can check if we're in an - // attribute with that name. It wouldn't be 100% correct (because of annoying things - // like using aliases), but would likely be good enough for the incremental cases in - // the IDE. - } + if (before.IsToken) + { + Debug.Assert(after.IsToken); + return AreTokensEquivalent(before, after, ignoreChildNode); + } - if (ignoreChildNode != null) - { - var e1 = before.ChildNodesAndTokens().GetEnumerator(); - var e2 = after.ChildNodesAndTokens().GetEnumerator(); - while (true) + if (topLevel) { - GreenNode? child1 = null; - GreenNode? child2 = null; + // Once we get down to the body level we don't need to go any further and we can + // consider these trees equivalent. + switch ((SyntaxKind)before.RawKind) + { + case SyntaxKind.Block: + case SyntaxKind.ArrowExpressionClause: + return AreNullableDirectivesEquivalent(before, after, ignoreChildNode); + } - // skip ignored children: - while (e1.MoveNext()) + // If we're only checking top level equivalence, then we don't have to go down into + // the initializer for a field. However, we can't put that optimization for all + // fields. For example, fields that are 'const' do need their initializers checked as + // changing them can affect binding results. + if ((SyntaxKind)before.RawKind == SyntaxKind.FieldDeclaration) { - var c = e1.Current; - if (c != null && (c.IsToken || !ignoreChildNode((SyntaxKind)c.RawKind))) + var fieldBefore = (Green.FieldDeclarationSyntax)before; + var fieldAfter = (Green.FieldDeclarationSyntax)after; + + var isConstBefore = fieldBefore.Modifiers.Any((int)SyntaxKind.ConstKeyword); + var isConstAfter = fieldAfter.Modifiers.Any((int)SyntaxKind.ConstKeyword); + + if (!isConstBefore && !isConstAfter) { - child1 = c; - break; + ignoreChildNode = static childKind => childKind == SyntaxKind.EqualsValueClause; } } - while (e2.MoveNext()) + // NOTE(cyrusn): Do we want to avoid going down into attribute expressions? I don't + // think we can avoid it as there are likely places in the compiler that use these + // expressions. For example, if the user changes [InternalsVisibleTo("goo")] to + // [InternalsVisibleTo("bar")] then that must count as a top level change as it + // affects symbol visibility. Perhaps we could enumerate the places in the compiler + // that use the values inside source attributes and we can check if we're in an + // attribute with that name. It wouldn't be 100% correct (because of annoying things + // like using aliases), but would likely be good enough for the incremental cases in + // the IDE. + } + + if (ignoreChildNode != null) + { + var e1 = before.ChildNodesAndTokens().GetEnumerator(); + var e2 = after.ChildNodesAndTokens().GetEnumerator(); + while (true) { - var c = e2.Current; - if (c != null && (c.IsToken || !ignoreChildNode((SyntaxKind)c.RawKind))) + GreenNode? child1 = null; + GreenNode? child2 = null; + + // skip ignored children: + while (e1.MoveNext()) { - child2 = c; - break; + var c = e1.Current; + if (c != null && (c.IsToken || !ignoreChildNode((SyntaxKind)c.RawKind))) + { + child1 = c; + break; + } } - } - if (child1 == null || child2 == null) - { - // false if some children remained - return child1 == child2; - } + while (e2.MoveNext()) + { + var c = e2.Current; + if (c != null && (c.IsToken || !ignoreChildNode((SyntaxKind)c.RawKind))) + { + child2 = c; + break; + } + } - if (!AreEquivalentRecursive(child1, child2, ignoreChildNode, topLevel)) - { - return false; - } - } - } - else - { - // simple comparison - not ignoring children + if (child1 == null || child2 == null) + { + // false if some children remained + return child1 == child2; + } - int slotCount = before.SlotCount; - if (slotCount != after.SlotCount) - { - return false; + stack.Push((child1, child2)); + } } - - for (int i = 0; i < slotCount; i++) + else { - var child1 = before.GetSlot(i); - var child2 = after.GetSlot(i); + // simple comparison - not ignoring children - if (!AreEquivalentRecursive(child1, child2, ignoreChildNode, topLevel)) + int slotCount = before.SlotCount; + if (slotCount != after.SlotCount) { return false; } + + // Walk the children backwards so that we can push them onto the stack and continue walking in DFS order. + for (var i = slotCount - 1; i >= 0; i--) + { + var child1 = before.GetSlot(i); + var child2 = after.GetSlot(i); + stack.Push((child1, child2)); + } } + // So far these are equivalent. Continue checking the children. return true; } } diff --git a/src/Compilers/CSharp/Test/Semantic/Semantics/RefEscapingTests.cs b/src/Compilers/CSharp/Test/Semantic/Semantics/RefEscapingTests.cs index 6da08b9784005..0cd3ee133280d 100644 --- a/src/Compilers/CSharp/Test/Semantic/Semantics/RefEscapingTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Semantics/RefEscapingTests.cs @@ -7871,6 +7871,37 @@ ref struct S Diagnostic(ErrorCode.ERR_EscapeVariable, "s").WithArguments("s").WithLocation(47, 16)); } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] + public void UserDefinedBinaryOperator_RefStruct_Nested() + { + var source = """ + class C + { + S M() + { + S s; + s = default(S) + 100 + 200; + return s; + } + } + + ref struct S + { + public static S operator+(S y, in int x) => throw null; + } + """; + CreateCompilation(source).VerifyDiagnostics( + // (6,13): error CS8347: Cannot use a result of 'S.operator +(S, in int)' in this context because it may expose variables referenced by parameter 'x' outside of their declaration scope + // s = default(S) + 100 + 200; + Diagnostic(ErrorCode.ERR_EscapeCall, "default(S) + 100").WithArguments("S.operator +(S, in int)", "x").WithLocation(6, 13), + // (6,13): error CS8347: Cannot use a result of 'S.operator +(S, in int)' in this context because it may expose variables referenced by parameter 'y' outside of their declaration scope + // s = default(S) + 100 + 200; + Diagnostic(ErrorCode.ERR_EscapeCall, "default(S) + 100 + 200").WithArguments("S.operator +(S, in int)", "y").WithLocation(6, 13), + // (6,26): error CS8156: An expression cannot be used in this context because it may not be passed or returned by reference + // s = default(S) + 100 + 200; + Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "100").WithLocation(6, 26)); + } + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/71773")] public void UserDefinedBinaryOperator_RefStruct_Scoped_Left() { @@ -8531,5 +8562,16 @@ static R F2() // return new R(1) | new R(2); Diagnostic(ErrorCode.ERR_RefReturnLvalueExpected, "1").WithLocation(18, 22)); } + + [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/72873")] + public void Utf8Addition() + { + var code = """ + using System; + ReadOnlySpan x = "Hello"u8 + " "u8 + "World!"u8; + Console.WriteLine(x.Length); + """; + CreateCompilation(code, targetFramework: TargetFramework.Net70).VerifyDiagnostics(); + } } } diff --git a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxEquivalenceTests.cs b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxEquivalenceTests.cs index 00f2a1223c791..4094cc28447d9 100644 --- a/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxEquivalenceTests.cs +++ b/src/Compilers/CSharp/Test/Syntax/Syntax/SyntaxEquivalenceTests.cs @@ -5,6 +5,7 @@ #nullable disable using System; +using System.Linq; using Roslyn.Test.Utilities; using Xunit; @@ -1284,5 +1285,61 @@ void M() VerifyNotEquivalent(tree1, tree2, topLevel: false); VerifyEquivalent(tree1, tree2, topLevel: true); } + + [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2018744")] + public void TestDeeplyNested1() + { + var expr = string.Join(" + ", Enumerable.Range(0, 10000).Select(_ => "a")); + + var tree1 = SyntaxFactory.ParseSyntaxTree($$"""" + class C + { + void M(int a, int b, int c) + { + var v = {{expr}} + b; + } + } + """"); + + var tree2 = SyntaxFactory.ParseSyntaxTree($$"""" + class C + { + void M(int a, int b, int c) + { + var v = {{expr}} + c; + } + } + """"); + + VerifyNotEquivalent(tree1, tree2, topLevel: false); + } + + [Fact, WorkItem("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2018744")] + public void TestDeeplyNested2() + { + var expr = string.Join(" + ", Enumerable.Range(0, 10000).Select(_ => "a")); + + var tree1 = SyntaxFactory.ParseSyntaxTree($$"""" + class C + { + void M(int a) + { + var v = {{expr}}; + } + } + """"); + + var tree2 = SyntaxFactory.ParseSyntaxTree($$"""" + class C + { + void M(int a) + { + var v = {{expr}}; + } + } + """"); + + VerifyEquivalent(tree1, tree2, topLevel: false); + } } } diff --git a/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs b/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs index d309e6fad88f4..0fb4efa394de5 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Analyzers/AnalyzerConfigTests.cs @@ -1083,6 +1083,28 @@ public void MultipleEditorConfigs() }, options.Select(o => o.TreeOptions).ToArray()); } + [Fact] + public void FolderNamePrefixOfFileName() + { + var configs = ArrayBuilder.GetInstance(); + configs.Add(Parse(@" +[*.cs] +dotnet_diagnostic.cs000.severity = suggestion", "/root/.editorconfig")); + configs.Add(Parse(@" +root=true", "/root/test/.editorconfig")); + + var options = GetAnalyzerConfigOptions( + new[] { "/root/testing.cs" }, + configs); + configs.Free(); + + Assert.Equal(new[] + { + CreateImmutableDictionary( + ("cs000", ReportDiagnostic.Info)), + }, options.Select(o => o.TreeOptions).ToArray()); + } + [Fact] public void InheritOuterConfig() { diff --git a/src/Compilers/Core/CodeAnalysisTest/FileSystem/PathUtilitiesTests.cs b/src/Compilers/Core/CodeAnalysisTest/FileSystem/PathUtilitiesTests.cs index c660ffa62b568..123f0ba58d5d1 100644 --- a/src/Compilers/Core/CodeAnalysisTest/FileSystem/PathUtilitiesTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/FileSystem/PathUtilitiesTests.cs @@ -4,6 +4,7 @@ #nullable disable +using System; using System.IO; using Roslyn.Test.Utilities; using Roslyn.Utilities; @@ -291,6 +292,26 @@ public void IsSameDirectoryOrChildOfNegativeTests() Assert.False(PathUtilities.IsSameDirectoryOrChildOf(@"C:\A\B\C", @"C:\A\B\C\D")); } + [ConditionalFact(typeof(WindowsOnly))] + public void IsSameDirectoryOrChildOfSpecifyingCaseSensitivity_Windows() + { + Assert.True(PathUtilities.IsSameDirectoryOrChildOf(@"C:\a\B\C", @"C:\A\B", StringComparison.OrdinalIgnoreCase)); + Assert.True(PathUtilities.IsSameDirectoryOrChildOf(@"C:\A\b\C", @"C:\A\B", StringComparison.OrdinalIgnoreCase)); + + Assert.False(PathUtilities.IsSameDirectoryOrChildOf(@"C:\a\B\C", @"C:\A\B", StringComparison.Ordinal)); + Assert.False(PathUtilities.IsSameDirectoryOrChildOf(@"C:\A\b\C", @"C:\A\B", StringComparison.Ordinal)); + } + + [ConditionalFact(typeof(UnixLikeOnly))] + public void IsSameDirectoryOrChildOfSpecifyingCaseSensitivity_Unix() + { + Assert.True(PathUtilities.IsSameDirectoryOrChildOf(@"/a/B/C", @"/A/B", StringComparison.OrdinalIgnoreCase)); + Assert.True(PathUtilities.IsSameDirectoryOrChildOf(@"/A/b/C", @"/A/B", StringComparison.OrdinalIgnoreCase)); + + Assert.False(PathUtilities.IsSameDirectoryOrChildOf(@"/a/B/C", @"/A/B", StringComparison.Ordinal)); + Assert.False(PathUtilities.IsSameDirectoryOrChildOf(@"/A/b/C", @"/A/B", StringComparison.Ordinal)); + } + [Fact] public void IsValidFilePath() { diff --git a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs index eaa8de216fe95..2296b51ec33ce 100644 --- a/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs +++ b/src/Compilers/Core/Portable/CommandLine/AnalyzerConfigSet.cs @@ -204,7 +204,7 @@ public AnalyzerConfigOptionsResult GetOptionsForSourcePath(string sourcePath) { var config = _analyzerConfigs[analyzerConfigIndex]; - if (normalizedPath.StartsWith(config.NormalizedDirectory, StringComparison.Ordinal)) + if (PathUtilities.IsSameDirectoryOrChildOf(normalizedPath, config.NormalizedDirectory, StringComparison.Ordinal)) { // If this config is a root config, then clear earlier options since they don't apply // to this source file. diff --git a/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs b/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs index 0617a16e08dfd..570121ad3d3bb 100644 --- a/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs +++ b/src/Compilers/Core/Portable/FileSystem/PathUtilities.cs @@ -159,7 +159,7 @@ public static string RemoveExtension(string path) return null; } - internal static bool IsSameDirectoryOrChildOf(string child, string parent) + internal static bool IsSameDirectoryOrChildOf(string child, string parent, StringComparison comparison = StringComparison.OrdinalIgnoreCase) { parent = RemoveTrailingDirectorySeparator(parent); string? currentChild = child; @@ -167,7 +167,7 @@ internal static bool IsSameDirectoryOrChildOf(string child, string parent) { currentChild = RemoveTrailingDirectorySeparator(currentChild); - if (currentChild.Equals(parent, StringComparison.OrdinalIgnoreCase)) + if (currentChild.Equals(parent, comparison)) { return true; } diff --git a/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs b/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs index 2b955627c7b21..4986a0098536b 100644 --- a/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs +++ b/src/EditorFeatures/CSharp/AutomaticCompletion/AutomaticLineEnderCommandHandler_Helpers.cs @@ -155,7 +155,7 @@ WhileStatementSyntax or ForEachStatementSyntax or ForStatementSyntax or LockStat oldNode: embeddedStatementOwner, newNode: AddBlockToEmbeddedStatementOwner(embeddedStatementOwner, formattingOptions), anchorNode: embeddedStatementOwner, - nodesToInsert: ImmutableArray.Empty.Add(statement), + nodesToInsert: [statement], formattingOptions, cancellationToken), DoStatementSyntax doStatementNode => AddBraceToDoStatement(services, root, doStatementNode, formattingOptions, statement, cancellationToken), @@ -195,7 +195,7 @@ private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToDoStatement oldNode: doStatementNode, newNode: AddBlockToEmbeddedStatementOwner(doStatementNode, formattingOptions), anchorNode: doStatementNode, - nodesToInsert: ImmutableArray.Empty.Add(innerStatement), + nodesToInsert: [innerStatement], formattingOptions, cancellationToken); } @@ -251,7 +251,7 @@ private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToIfStatement ifStatementNode, AddBlockToEmbeddedStatementOwner(ifStatementNode, formattingOptions), ifStatementNode, - ImmutableArray.Empty.Add(innerStatement), + [innerStatement], formattingOptions, cancellationToken); } @@ -319,7 +319,7 @@ private static (SyntaxNode newRoot, int nextCaretPosition) AddBraceToElseClause( elseClauseNode, WithBraces(elseClauseNode, formattingOptions), elseClauseNode.Parent!, - ImmutableArray.Empty.Add(innerStatement), + [innerStatement], formattingOptions, cancellationToken); } diff --git a/src/EditorFeatures/CSharpTest/InlineDiagnostics/InlineDiagnosticsTaggerProviderTests.cs b/src/EditorFeatures/CSharpTest/InlineDiagnostics/InlineDiagnosticsTaggerProviderTests.cs index 339bf5c7e32b2..8462f9b447d79 100644 --- a/src/EditorFeatures/CSharpTest/InlineDiagnostics/InlineDiagnosticsTaggerProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/InlineDiagnostics/InlineDiagnosticsTaggerProviderTests.cs @@ -55,6 +55,6 @@ private static async Task>> GetTag private static async Task>> GetTagSpansAsync(EditorTestWorkspace workspace) { workspace.GlobalOptions.SetGlobalOption(InlineDiagnosticsOptionsStorage.EnableInlineDiagnostics, LanguageNames.CSharp, true); - return (await TestDiagnosticTagProducer.GetDiagnosticsAndErrorSpans(workspace)).Item2; + return await TestDiagnosticTagProducer.GetTagSpansAsync(workspace); } } diff --git a/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToSearcherTests.cs b/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToSearcherTests.cs index 24c066414e32d..fca8d0d529356 100644 --- a/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToSearcherTests.cs +++ b/src/EditorFeatures/CSharpTest/NavigateTo/NavigateToSearcherTests.cs @@ -7,8 +7,8 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Editor.UnitTests; -using Microsoft.CodeAnalysis.Editor.UnitTests.NavigateTo; using Microsoft.CodeAnalysis.NavigateTo; using Microsoft.CodeAnalysis.Navigation; using Microsoft.CodeAnalysis.PatternMatching; @@ -24,7 +24,7 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.NavigateTo [Trait(Traits.Feature, Traits.Features.NavigateTo)] public sealed class NavigateToSearcherTests { - private static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(FirstDocIsActiveAndVisibleDocumentTrackingService.Factory)); + private static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(FirstDocumentIsActiveAndVisibleDocumentTrackingService.Factory)); private static void SetupSearchProject( Mock searchService, diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs index 5cd92953a1c02..482304aa00701 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/Dashboard/RenameDashboard.xaml.cs @@ -52,7 +52,7 @@ public RenameDashboard( _model = model; InitializeComponent(); - _tabNavigableChildren = new UIElement[] { this.OverloadsCheckbox, this.CommentsCheckbox, this.StringsCheckbox, this.FileRenameCheckbox, this.PreviewChangesCheckbox, this.ApplyButton, this.CloseButton }.ToList(); + _tabNavigableChildren = [this.OverloadsCheckbox, this.CommentsCheckbox, this.StringsCheckbox, this.FileRenameCheckbox, this.PreviewChangesCheckbox, this.ApplyButton, this.CloseButton]; _textView = textView; this.DataContext = model; diff --git a/src/EditorFeatures/Core.Wpf/QuickInfo/ProjectionBufferContent.cs b/src/EditorFeatures/Core.Wpf/QuickInfo/ProjectionBufferContent.cs index 9cfa06e7b685b..6c18010ae0371 100644 --- a/src/EditorFeatures/Core.Wpf/QuickInfo/ProjectionBufferContent.cs +++ b/src/EditorFeatures/Core.Wpf/QuickInfo/ProjectionBufferContent.cs @@ -97,7 +97,7 @@ private IWpfTextView CreateView(ITextBuffer buffer) private IProjectionBuffer CreateBuffer() { return _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( - _editorOptionsService.Factory.GlobalOptions, _contentType, _spans.ToArray()); + _editorOptionsService.Factory.GlobalOptions, _contentType, [.. _spans]); } } } diff --git a/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs b/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs index be975381ed17e..f4f2be82f379a 100644 --- a/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs +++ b/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource.cs @@ -21,6 +21,7 @@ using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; +using Microsoft.VisualStudio.Threading; using Roslyn.Utilities; using IUIThreadOperationContext = Microsoft.VisualStudio.Utilities.IUIThreadOperationContext; @@ -60,11 +61,7 @@ public SuggestedActionsSource( } public void Dispose() - { - _state.Dispose(); - } - - private ReferenceCountedDisposable SourceState => _state; + => _state.Dispose(); public bool TryGetTelemetryId(out Guid telemetryId) { @@ -122,21 +119,6 @@ public bool TryGetTelemetryId(out Guid telemetryId) IUIThreadOperationContext operationContext) => null; - private static string GetFixCategory(DiagnosticSeverity severity) - { - switch (severity) - { - case DiagnosticSeverity.Hidden: - case DiagnosticSeverity.Info: - case DiagnosticSeverity.Warning: - return PredefinedSuggestedActionCategoryNames.CodeFix; - case DiagnosticSeverity.Error: - return PredefinedSuggestedActionCategoryNames.ErrorFix; - default: - throw ExceptionUtilities.Unreachable(); - } - } - public Task HasSuggestedActionsAsync( ISuggestedActionCategorySet requestedActionCategories, SnapshotSpan range, @@ -146,143 +128,6 @@ public Task HasSuggestedActionsAsync( throw new NotImplementedException($"We implement {nameof(GetSuggestedActionCategoriesAsync)}. This should not be called."); } - private async Task GetSpanAsync(ReferenceCountedDisposable state, SnapshotSpan range, CancellationToken cancellationToken) - { - // First, ensure that the snapshot we're being asked about is for an actual - // roslyn document. This can fail, for example, in projection scenarios where - // we are called with a range snapshot that refers to the projection buffer - // and not the actual roslyn code that is being projected into it. - var document = range.Snapshot.GetOpenTextDocumentInCurrentContextWithChanges(); - if (document == null) - { - return null; - } - - // Also make sure the range is from the same buffer that this source was created for - Contract.ThrowIfFalse( - range.Snapshot.TextBuffer.Equals(state.Target.SubjectBuffer), - $"Invalid text buffer passed to {nameof(HasSuggestedActionsAsync)}"); - - // Next, before we do any async work, acquire the user's selection, directly grabbing - // it from the UI thread if that's what we're on. That way we don't have any reentrancy - // blocking concerns if VS wants to block on this call (for example, if the user - // explicitly invokes the 'show smart tag' command). - // - // This work must happen on the UI thread as it needs to access the _textView's mutable - // state. - // - // Note: we may be called in one of two VS scenarios: - // 1) User has moved caret to a new line. In this case VS will call into us in the - // bg to see if we have any suggested actions for this line. In order to figure - // this out, we need to see what selection the user has (for refactorings), which - // necessitates going back to the fg. - // - // 2) User moves to a line and immediately hits ctrl-dot. In this case, on the UI - // thread VS will kick us off and then immediately block to get the results so - // that they can expand the light-bulb. In this case we cannot do BG work first, - // then call back into the UI thread to try to get the user selection. This will - // deadlock as the UI thread is blocked on us. - // - // There are two solution to '2'. Either introduce reentrancy (which we really don't - // like to do), or just ensure that we acquire and get the users selection up front. - // This means that when we're called from the UI thread, we never try to go back to the - // UI thread. - TextSpan? selection = null; - if (_threadingContext.JoinableTaskContext.IsOnMainThread) - { - selection = TryGetCodeRefactoringSelection(state, range); - } - else - { - await _threadingContext.InvokeBelowInputPriorityAsync(() => - { - // Make sure we were not disposed between kicking off this work and getting to this point. - using var state = _state.TryAddReference(); - if (state is null) - return; - - selection = TryGetCodeRefactoringSelection(state, range); - }, cancellationToken).ConfigureAwait(false); - } - - return selection; - } - - private static async Task GetFixLevelAsync( - ReferenceCountedDisposable state, - TextDocument document, - SnapshotSpan range, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - var lowPriorityAnalyzers = new ConcurrentSet(); - - foreach (var order in Orderings) - { - var priority = TryGetPriority(order); - Contract.ThrowIfNull(priority); - var priorityProvider = new SuggestedActionPriorityProvider(priority.Value, lowPriorityAnalyzers); - - var result = await GetFixLevelAsync(priorityProvider).ConfigureAwait(false); - if (result != null) - return result; - } - - return null; - - async Task GetFixLevelAsync(ICodeActionRequestPriorityProvider priorityProvider) - { - if (state.Target.Owner._codeFixService != null && - state.Target.SubjectBuffer.SupportsCodeFixes()) - { - var result = await state.Target.Owner._codeFixService.GetMostSevereFixAsync( - document, range.Span.ToTextSpan(), priorityProvider, fallbackOptions, cancellationToken).ConfigureAwait(false); - - if (result.HasFix) - { - Logger.Log(FunctionId.SuggestedActions_HasSuggestedActionsAsync); - return GetFixCategory(result.CodeFixCollection.FirstDiagnostic.Severity); - } - - if (!result.UpToDate) - return null; - } - - return null; - } - } - - private async Task TryGetRefactoringSuggestedActionCategoryAsync( - TextDocument document, - TextSpan? selection, - CodeActionOptionsProvider fallbackOptions, - CancellationToken cancellationToken) - { - using var state = _state.TryAddReference(); - if (state is null) - return null; - - if (!selection.HasValue) - { - // this is here to fail test and see why it is failed. - Trace.WriteLine("given range is not current"); - return null; - } - - if (GlobalOptions.GetOption(EditorComponentOnOffOptions.CodeRefactorings) && - state.Target.Owner._codeRefactoringService != null && - state.Target.SubjectBuffer.SupportsRefactorings()) - { - if (await state.Target.Owner._codeRefactoringService.HasRefactoringsAsync( - document, selection.Value, fallbackOptions, cancellationToken).ConfigureAwait(false)) - { - return PredefinedSuggestedActionCategoryNames.Refactoring; - } - } - - return null; - } - private TextSpan? TryGetCodeRefactoringSelection(ReferenceCountedDisposable state, SnapshotSpan range) { _threadingContext.ThrowIfNotOnUIThread(); @@ -294,17 +139,13 @@ await _threadingContext.InvokeBelowInputPriorityAsync(() => // We only support refactorings when there is a single selection in the document. if (selectedSpans.Count != 1) - { return null; - } var translatedSpan = selectedSpans[0].TranslateTo(range.Snapshot, SpanTrackingMode.EdgeInclusive); // We only support refactorings when selected span intersects with the span that the light bulb is asking for. if (!translatedSpan.IntersectsWith(range)) - { return null; - } return translatedSpan.Span.ToTextSpan(); } @@ -318,6 +159,11 @@ private void OnTextViewClosed(object sender, EventArgs e) if (state is null) return null; + // Make sure the range is from the same buffer that this source was created for. + Contract.ThrowIfFalse( + range.Snapshot.TextBuffer.Equals(state.Target.SubjectBuffer), + $"Invalid text buffer passed to {nameof(HasSuggestedActionsAsync)}"); + var workspace = state.Target.Workspace; if (workspace == null) return null; @@ -336,18 +182,22 @@ private void OnTextViewClosed(object sender, EventArgs e) var fallbackOptions = GlobalOptions.GetCodeActionOptionsProvider(); using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - var linkedToken = linkedTokenSource.Token; + // Assign over cancellation token so no one accidentally uses the wrong token. + cancellationToken = linkedTokenSource.Token; - var errorTask = Task.Run(() => GetFixLevelAsync(state, document, range, fallbackOptions, linkedToken), linkedToken); + // Kick off the work to get errors. + var errorTask = GetFixLevelAsync(); - var selection = await GetSpanAsync(state, range, linkedToken).ConfigureAwait(false); + // Make a quick jump back to the UI thread to get the user's selection, then go back to the thread pool.. + await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); - var refactoringTask = SpecializedTasks.Null(); - if (selection != null) - { - refactoringTask = Task.Run( - () => TryGetRefactoringSuggestedActionCategoryAsync(document, selection, fallbackOptions, linkedToken), linkedToken); - } + var selection = TryGetCodeRefactoringSelection(state, range); + await TaskScheduler.Default; + + // If we have a selection, kick off the work to get refactorings concurrently with the above work to get errors. + var refactoringTask = selection != null + ? TryGetRefactoringSuggestedActionCategoryAsync(selection) + : SpecializedTasks.Null(); // If we happen to get the result of the error task before the refactoring task, // and that result is non-null, we can just cancel the refactoring task. @@ -357,6 +207,79 @@ private void OnTextViewClosed(object sender, EventArgs e) return result == null ? null : _suggestedActionCategoryRegistry.CreateSuggestedActionCategorySet(result); + + async Task GetFixLevelAsync() + { + // Ensure we yield the thread that called into us, allowing it to continue onwards. + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + var lowPriorityAnalyzers = new ConcurrentSet(); + + foreach (var order in Orderings) + { + var priority = TryGetPriority(order); + Contract.ThrowIfNull(priority); + var priorityProvider = new SuggestedActionPriorityProvider(priority.Value, lowPriorityAnalyzers); + + var result = await GetFixCategoryAsync(priorityProvider).ConfigureAwait(false); + if (result != null) + return result; + } + + return null; + } + + async Task GetFixCategoryAsync(ICodeActionRequestPriorityProvider priorityProvider) + { + if (state.Target.Owner._codeFixService != null && + state.Target.SubjectBuffer.SupportsCodeFixes()) + { + var result = await state.Target.Owner._codeFixService.GetMostSevereFixAsync( + document, range.Span.ToTextSpan(), priorityProvider, fallbackOptions, cancellationToken).ConfigureAwait(false); + + if (result.HasFix) + { + Logger.Log(FunctionId.SuggestedActions_HasSuggestedActionsAsync); + return result.CodeFixCollection.FirstDiagnostic.Severity switch + { + + DiagnosticSeverity.Hidden or DiagnosticSeverity.Info or DiagnosticSeverity.Warning => PredefinedSuggestedActionCategoryNames.CodeFix, + DiagnosticSeverity.Error => PredefinedSuggestedActionCategoryNames.ErrorFix, + _ => throw ExceptionUtilities.Unreachable(), + }; + } + + if (!result.UpToDate) + return null; + } + + return null; + } + + async Task TryGetRefactoringSuggestedActionCategoryAsync(TextSpan? selection) + { + // Ensure we yield the thread that called into us, allowing it to continue onwards. + await TaskScheduler.Default.SwitchTo(alwaysYield: true); + + if (!selection.HasValue) + { + // this is here to fail test and see why it is failed. + Trace.WriteLine("given range is not current"); + return null; + } + + if (GlobalOptions.GetOption(EditorComponentOnOffOptions.CodeRefactorings) && + state.Target.Owner._codeRefactoringService != null && + state.Target.SubjectBuffer.SupportsRefactorings()) + { + if (await state.Target.Owner._codeRefactoringService.HasRefactoringsAsync( + document, selection.Value, fallbackOptions, cancellationToken).ConfigureAwait(false)) + { + return PredefinedSuggestedActionCategoryNames.Refactoring; + } + } + + return null; + } } } } diff --git a/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource_Async.cs b/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource_Async.cs index 1a78c33ae59e2..8745781c048a2 100644 --- a/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource_Async.cs +++ b/src/EditorFeatures/Core.Wpf/Suggestions/SuggestedActionsSource_Async.cs @@ -70,7 +70,7 @@ private async Task GetSuggestedActionsWorkerAsync( CancellationToken cancellationToken) { _threadingContext.ThrowIfNotOnUIThread(); - using var state = SourceState.TryAddReference(); + using var state = _state.TryAddReference(); if (state is null) return; diff --git a/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs b/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs index c8c5859d075d5..f025bbca1dd3d 100644 --- a/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs +++ b/src/EditorFeatures/Core/CommentSelection/AbstractToggleBlockCommentBase.cs @@ -436,7 +436,7 @@ private ImmutableArray GetUncommentedSpansInSelection() } } - return uncommentedSpans.ToImmutableArray(); + return [.. uncommentedSpans]; } } } diff --git a/src/EditorFeatures/Core/Editor/TextEditApplication.cs b/src/EditorFeatures/Core/Editor/TextEditApplication.cs index c4bf5575e8f83..c0acaa4266745 100644 --- a/src/EditorFeatures/Core/Editor/TextEditApplication.cs +++ b/src/EditorFeatures/Core/Editor/TextEditApplication.cs @@ -17,7 +17,7 @@ internal static void UpdateText(SourceText newText, ITextBuffer buffer, EditOpti var oldSnapshot = buffer.CurrentSnapshot; var oldText = oldSnapshot.AsText(); var changes = newText.GetTextChanges(oldText); - UpdateText(changes.ToImmutableArray(), buffer, oldSnapshot, oldText, options); + UpdateText([.. changes], buffer, oldSnapshot, oldText, options); } public static void UpdateText(ImmutableArray textChanges, ITextBuffer buffer, EditOptions options) diff --git a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs index ead1708f6bec9..0395dfb45c7fc 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/Aggregator/SettingsAggregator.cs @@ -93,7 +93,7 @@ private static ISettingsProviderFactory GetOptionsProviderFactory(Workspac TryAddProviderForLanguage(LanguageNames.VisualBasic, workspace, providers); } - return new CombinedOptionsProviderFactory(providers.ToImmutableArray()); + return new CombinedOptionsProviderFactory([.. providers]); static void TryAddProviderForLanguage(string language, Workspace workspace, List> providers) { diff --git a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs index fb310e0fc78f7..b1a04b9621a9f 100644 --- a/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs +++ b/src/EditorFeatures/Core/EditorConfigSettings/DataProvider/SettingsProviderBase.cs @@ -88,7 +88,7 @@ public ImmutableArray GetCurrentDataSnapshot() { lock (s_gate) { - return _snapshot.ToImmutableArray(); + return [.. _snapshot]; } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs index 78d641cebbfc2..cfc295a276a4d 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptInProcLanguageClient.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; using Microsoft.CodeAnalysis.Options; using Microsoft.VisualStudio.Composition; using Microsoft.VisualStudio.LanguageServer.Client; @@ -57,6 +58,18 @@ public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapa serverCapabilities.ProjectContextProvider = true; serverCapabilities.SupportsDiagnosticRequests = true; + serverCapabilities.DiagnosticProvider = new() + { + SupportsMultipleContextsDiagnostics = true, + DiagnosticKinds = + [ + new(PullDiagnosticCategories.Task), + new(PullDiagnosticCategories.WorkspaceDocumentsAndProject), + new(PullDiagnosticCategories.DocumentAnalyzerSyntax), + new(PullDiagnosticCategories.DocumentAnalyzerSemantic), + ] + }; + return serverCapabilities; } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs index da2e663f06bfd..b5527e89596a0 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptPullDiagnosticHandlerProvider.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; @@ -17,8 +18,9 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal class VSTypeScriptDocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, - IGlobalOptionService globalOptions) : DocumentPullDiagnosticHandlerFactory(analyzerService, diagnosticsRefresher, globalOptions) + IGlobalOptionService globalOptions) : DocumentPullDiagnosticHandlerFactory(analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions) { } @@ -28,7 +30,8 @@ internal class VSTypeScriptDocumentPullDiagnosticHandlerFactory( internal class VSTypeScriptWorkspacePullDiagnosticHandler( LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, - IGlobalOptionService globalOptions) : WorkspacePullDiagnosticHandlerFactory(registrationService, analyzerService, diagnosticsRefresher, globalOptions) + IGlobalOptionService globalOptions) : WorkspacePullDiagnosticHandlerFactory(registrationService, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions) { } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs index fe13da5eaac87..4f30186d98c73 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs @@ -319,7 +319,7 @@ private void UpdateReferenceLocationsTask() // https://github.com/dotnet/roslyn/issues/40890 await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(alwaysYield: true, cancellationToken); - RaiseSessionSpansUpdated(inlineRenameLocations.Locations.ToImmutableArray()); + RaiseSessionSpansUpdated([.. inlineRenameLocations.Locations]); return inlineRenameLocations; }); diff --git a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs index 2f738faae61df..df4f024f6bedc 100644 --- a/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs +++ b/src/EditorFeatures/Core/IntelliSense/AsyncCompletion/CompletionSource.cs @@ -82,7 +82,7 @@ internal CompletionSource( _asyncListener = asyncListener; _editorOptionsService = editorOptionsService; _isDebuggerTextView = textView is IDebuggerTextView; - _roles = textView.Roles.ToImmutableHashSet(); + _roles = [.. textView.Roles]; } public AsyncCompletionData.CompletionStartData InitializeCompletion( diff --git a/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs b/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs index e4ce2a8eacb29..dacbc7633a275 100644 --- a/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs +++ b/src/EditorFeatures/Core/LanguageServer/AlwaysActivateInProcLanguageClient.cs @@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.LanguageServer.Handler.SemanticTokens; using Microsoft.CodeAnalysis.Options; using Microsoft.VisualStudio.Composition; @@ -40,9 +41,11 @@ internal class AlwaysActivateInProcLanguageClient( ILspServiceLoggerFactory lspLoggerFactory, IThreadingContext threadingContext, ExportProvider exportProvider, + IDiagnosticSourceManager diagnosticSourceManager, [ImportMany] IEnumerable> buildOnlyDiagnostics) : AbstractInProcLanguageClient(lspServiceProvider, globalOptions, lspLoggerFactory, threadingContext, exportProvider) { private readonly ExperimentalCapabilitiesProvider _experimentalCapabilitiesProvider = defaultCapabilitiesProvider; + private readonly IDiagnosticSourceManager _diagnosticSourceManager = diagnosticSourceManager; private readonly IEnumerable> _buildOnlyDiagnostics = buildOnlyDiagnostics; protected override ImmutableArray SupportedLanguages => ProtocolConstants.RoslynLspLanguages; @@ -69,28 +72,15 @@ public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapa serverCapabilities.SupportsDiagnosticRequests = true; serverCapabilities.DiagnosticProvider ??= new(); + + // VS does not distinguish between document and workspace diagnostics, so we need to merge them. + var diagnosticSourceNames = _diagnosticSourceManager.GetDocumentSourceProviderNames() + .Concat(_diagnosticSourceManager.GetWorkspaceSourceProviderNames()) + .Distinct(); 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), - new(PullDiagnosticCategories.EditAndContinue), - // 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), - ], + DiagnosticKinds = diagnosticSourceNames.Select(n => new VSInternalDiagnosticKind(n)).ToArray(), BuildOnlyDiagnosticIds = _buildOnlyDiagnostics .SelectMany(lazy => lazy.Metadata.BuildOnlyDiagnostics) .Distinct() @@ -118,7 +108,7 @@ public override ServerCapabilities GetCapabilities(ClientCapabilities clientCapa Range = true, Legend = new SemanticTokensLegend { - TokenTypes = SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes.ToArray(), + TokenTypes = [.. SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes], TokenModifiers = SemanticTokensSchema.TokenModifiers } }; diff --git a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs index abce7a9ad1c88..385423e295bc6 100644 --- a/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs +++ b/src/EditorFeatures/Core/NavigationBar/NavigationBarController.cs @@ -215,13 +215,13 @@ private void GetProjectItems(out ImmutableArray projec return; } - projectItems = documents.Select(d => + 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(); + language: d.Project.Language)).OrderBy(projectItem => projectItem.Text)]; var document = _subjectBuffer.AsTextContainer().GetOpenDocumentInCurrentContext(); selectedProjectItem = document != null diff --git a/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs b/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs index 75a264060fcc5..fae0c03b08895 100644 --- a/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs +++ b/src/EditorFeatures/Core/Preview/AbstractPreviewFactoryService.cs @@ -589,7 +589,7 @@ public Task> CreateRemovedAnalyzerCo oldBuffer.CurrentSnapshot, "...", description, - originalSpans.ToArray()); + [.. originalSpans]); var changedBuffer = _projectionBufferFactoryService.CreateProjectionBufferWithoutIndentation( _contentTypeRegistryService, @@ -597,7 +597,7 @@ public Task> CreateRemovedAnalyzerCo newBuffer.CurrentSnapshot, "...", description, - changedSpans.ToArray()); + [.. changedSpans]); #pragma warning disable CA2007 // Consider calling ConfigureAwait on the awaited task (containing method uses JTF) return await CreateNewDifferenceViewerAsync(leftWorkspace, rightWorkspace, originalBuffer, changedBuffer, zoomLevel, cancellationToken); diff --git a/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs b/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs index 92187c42e3748..805117ccedac8 100644 --- a/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs +++ b/src/EditorFeatures/Core/Remote/SolutionChecksumUpdater.cs @@ -28,18 +28,25 @@ internal sealed class SolutionChecksumUpdater /// private readonly IGlobalOperationNotificationService? _globalOperationService; + private readonly IDocumentTrackingService _documentTrackingService; + /// /// 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; + 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; + /// + /// Queue for kicking off the work to synchronize the active document to the remote process. + /// + private readonly AsyncBatchingWorkQueue _synchronizeActiveDocumentQueue; + private readonly object _gate = new(); private bool _isPaused; @@ -53,8 +60,9 @@ public SolutionChecksumUpdater( _globalOperationService = workspace.Services.SolutionServices.ExportProvider.GetExports().FirstOrDefault()?.Value; _workspace = workspace; + _documentTrackingService = workspace.Services.GetRequiredService(); - _textChangeQueue = new AsyncBatchingWorkQueue<(Document? oldDocument, Document? newDocument)>( + _textChangeQueue = new AsyncBatchingWorkQueue<(Document oldDocument, Document newDocument)>( DelayTimeSpan.NearImmediate, SynchronizeTextChangesAsync, listener, @@ -66,8 +74,15 @@ public SolutionChecksumUpdater( listener, shutdownToken); + _synchronizeActiveDocumentQueue = new AsyncBatchingWorkQueue( + DelayTimeSpan.NearImmediate, + SynchronizeActiveDocumentAsync, + listener, + shutdownToken); + // start listening workspace change event _workspace.WorkspaceChanged += OnWorkspaceChanged; + _documentTrackingService.ActiveDocumentChanged += OnActiveDocumentChanged; if (_globalOperationService != null) { @@ -84,6 +99,7 @@ public void Shutdown() // Try to stop any work that is in progress. PauseWork(); + _documentTrackingService.ActiveDocumentChanged -= OnActiveDocumentChanged; _workspace.WorkspaceChanged -= OnWorkspaceChanged; if (_globalOperationService != null) @@ -106,6 +122,7 @@ private void PauseWork() lock (_gate) { _synchronizeWorkspaceQueue.CancelExistingWork(); + _synchronizeActiveDocumentQueue.CancelExistingWork(); _isPaused = true; } } @@ -115,6 +132,7 @@ private void ResumeWork() lock (_gate) { _isPaused = false; + _synchronizeActiveDocumentQueue.AddWork(); _synchronizeWorkspaceQueue.AddWork(); } } @@ -131,12 +149,18 @@ private void OnWorkspaceChanged(object? sender, WorkspaceChangeEventArgs e) if (e.Kind == WorkspaceChangeKind.DocumentChanged) { - _textChangeQueue.AddWork((e.OldSolution.GetDocument(e.DocumentId), e.NewSolution.GetDocument(e.DocumentId))); + var oldDocument = e.OldSolution.GetDocument(e.DocumentId); + var newDocument = e.NewSolution.GetDocument(e.DocumentId); + if (oldDocument != null && newDocument != null) + _textChangeQueue.AddWork((oldDocument, newDocument)); } _synchronizeWorkspaceQueue.AddWork(); } + private void OnActiveDocumentChanged(object? sender, DocumentId? e) + => _synchronizeActiveDocumentQueue.AddWork(); + private async ValueTask SynchronizePrimaryWorkspaceAsync(CancellationToken cancellationToken) { var solution = _workspace.CurrentSolution; @@ -153,15 +177,26 @@ await client.TryInvokeAsync( } } + private async ValueTask SynchronizeActiveDocumentAsync(CancellationToken cancellationToken) + { + var activeDocument = _documentTrackingService.TryGetActiveDocument(); + + var client = await RemoteHostClient.TryGetClientAsync(_workspace, cancellationToken).ConfigureAwait(false); + if (client == null) + return; + + var solution = _workspace.CurrentSolution; + await client.TryInvokeAsync( + (service, cancellationToken) => service.SynchronizeActiveDocumentAsync(activeDocument, cancellationToken), + cancellationToken).ConfigureAwait(false); + } + private async ValueTask SynchronizeTextChangesAsync( - ImmutableSegmentedList<(Document? oldDocument, Document? newDocument)> values, + ImmutableSegmentedList<(Document oldDocument, Document newDocument)> values, CancellationToken cancellationToken) { foreach (var (oldDocument, newDocument) in values) { - if (oldDocument is null || newDocument is null) - continue; - cancellationToken.ThrowIfCancellationRequested(); await SynchronizeTextChangesAsync(oldDocument, newDocument, cancellationToken).ConfigureAwait(false); } @@ -194,15 +229,15 @@ async ValueTask SynchronizeTextChangesAsync(Document oldDocument, Document newDo } // get text changes - var textChanges = newText.GetTextChanges(oldText); - if (textChanges.Count == 0) + var textChanges = newText.GetTextChanges(oldText).AsImmutable(); + if (textChanges.Length == 0) { // no changes return; } // whole document case - if (textChanges.Count == 1 && textChanges[0].Span.Length == oldText.Length) + if (textChanges.Length == 1 && textChanges[0].Span.Length == oldText.Length) { // no benefit here. pulling from remote host is more efficient return; diff --git a/src/EditorFeatures/Core/Shared/Utilities/IThreadingContextExtensions.cs b/src/EditorFeatures/Core/Shared/Utilities/IThreadingContextExtensions.cs deleted file mode 100644 index 069816e08b4e7..0000000000000 --- a/src/EditorFeatures/Core/Shared/Utilities/IThreadingContextExtensions.cs +++ /dev/null @@ -1,57 +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.Diagnostics; -using System.Threading; -using System.Threading.Tasks; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; - -internal static class IThreadingContextExtensions -{ - /// - /// Returns true if any keyboard or mouse button input is pending on the message queue. - /// - public static bool IsInputPending() - { - // 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; - } - - public static Task InvokeBelowInputPriorityAsync(this IThreadingContext threadingContext, Action action, CancellationToken cancellationToken = default) - { - if (threadingContext.JoinableTaskContext.IsOnMainThread && !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); - } - } -} diff --git a/src/EditorFeatures/Core/Shared/Utilities/IUIContextActivationService.cs b/src/EditorFeatures/Core/Shared/Utilities/IUIContextActivationService.cs new file mode 100644 index 0000000000000..a68e544abd1b4 --- /dev/null +++ b/src/EditorFeatures/Core/Shared/Utilities/IUIContextActivationService.cs @@ -0,0 +1,15 @@ +// 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; + +namespace Microsoft.CodeAnalysis.Editor.Shared.Utilities; + +internal interface IUIContextActivationService +{ + /// + /// Executes the specified action when the UIContext first becomes active, or immediately if it is already active + /// + void ExecuteWhenActivated(Guid uiContext, Action action); +} diff --git a/src/EditorFeatures/Test/CodeFixes/CodeFixServiceTests.cs b/src/EditorFeatures/Test/CodeFixes/CodeFixServiceTests.cs index 7cafd90ce14eb..0594aa086c502 100644 --- a/src/EditorFeatures/Test/CodeFixes/CodeFixServiceTests.cs +++ b/src/EditorFeatures/Test/CodeFixes/CodeFixServiceTests.cs @@ -1079,8 +1079,9 @@ void M() ? root.DescendantNodes().OfType().First().Span : root.DescendantNodes().OfType().First().Span; - await diagnosticIncrementalAnalyzer.GetDiagnosticsAsync( - sourceDocument.Project.Solution, sourceDocument.Project.Id, sourceDocument.Id, includeSuppressedDiagnostics: true, includeNonLocalDocumentDiagnostics: true, CancellationToken.None); + await diagnosticIncrementalAnalyzer.GetDiagnosticsForIdsAsync( + sourceDocument.Project.Solution, sourceDocument.Project.Id, sourceDocument.Id, diagnosticIds: null, shouldIncludeAnalyzer: null, getDocuments: null, + includeSuppressedDiagnostics: true, includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, CancellationToken.None); await diagnosticIncrementalAnalyzer.GetTestAccessor().TextDocumentOpenAsync(sourceDocument); var lowPriorityAnalyzers = new ConcurrentSet(); diff --git a/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs b/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs index 687c123b63ba3..8541cd06e00ff 100644 --- a/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs +++ b/src/EditorFeatures/Test/CodeFixes/ExtensionOrderingTests.cs @@ -48,7 +48,7 @@ public void TestNoCyclesInFixProviders() var vbProviders = providersPerLanguage[LanguageNames.VisualBasic]; ExtensionOrderer.TestAccessor.CheckForCycles(vbProviders); - actualOrder = ExtensionOrderer.Order(vbProviders).ToArray(); + actualOrder = [.. ExtensionOrderer.Order(vbProviders)]; Assert.True(actualOrder.Length > 0); Assert.True(actualOrder.IndexOf(p => p.Metadata.Name == PredefinedCodeFixProviderNames.AddImport) < actualOrder.IndexOf(p => p.Metadata.Name == PredefinedCodeFixProviderNames.FullyQualify)); @@ -106,7 +106,7 @@ public void TestNoCyclesInRefactoringProviders() var vbProviders = providersPerLanguage[LanguageNames.VisualBasic]; ExtensionOrderer.TestAccessor.CheckForCycles(vbProviders); - actualOrder = ExtensionOrderer.Order(vbProviders).ToArray(); + actualOrder = [.. ExtensionOrderer.Order(vbProviders)]; Assert.True(actualOrder.Length > 0); } diff --git a/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs b/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs index 9fb93970e27cf..2c7e97f9e7084 100644 --- a/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs +++ b/src/EditorFeatures/Test/Diagnostics/DiagnosticAnalyzerServiceTests.cs @@ -12,20 +12,16 @@ using Microsoft.CodeAnalysis.CSharp.RemoveUnnecessarySuppressions; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Diagnostics.CSharp; -using Microsoft.CodeAnalysis.Diagnostics.EngineV2; using Microsoft.CodeAnalysis.Editor.Test; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Remote.Diagnostics; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Simplification; using Microsoft.CodeAnalysis.SolutionCrawler; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.UnitTests; using Roslyn.Test.Utilities; using Roslyn.Test.Utilities.TestGenerators; using Roslyn.Utilities; @@ -71,8 +67,9 @@ public async Task TestHasSuccessfullyLoadedBeingFalse() var analyzer = service.CreateIncrementalAnalyzer(workspace); var globalOptions = exportProvider.GetExportedValue(); - var diagnostics = await analyzer.GetDiagnosticsAsync( - workspace.CurrentSolution, projectId: null, documentId: null, includeSuppressedDiagnostics: false, includeNonLocalDocumentDiagnostics: false, CancellationToken.None); + var diagnostics = await analyzer.GetDiagnosticsForIdsAsync( + workspace.CurrentSolution, projectId: null, documentId: null, diagnosticIds: null, shouldIncludeAnalyzer: null, getDocuments: null, + includeSuppressedDiagnostics: false, includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: false, CancellationToken.None); Assert.NotEmpty(diagnostics); } diff --git a/src/EditorFeatures/Test/Emit/CompilationOutputsTests.cs b/src/EditorFeatures/Test/Emit/CompilationOutputsTests.cs index ca2b47721d8d9..434fcf3f2cd69 100644 --- a/src/EditorFeatures/Test/Emit/CompilationOutputsTests.cs +++ b/src/EditorFeatures/Test/Emit/CompilationOutputsTests.cs @@ -61,7 +61,7 @@ public void AssemblyAndPdb(DebugInformationFormat format) Stream? currentPdbStream = null; var outputs = new TestCompilationOutputs( - openAssemblyStream: () => currentPEStream = new MemoryStream(peImage.ToArray()), + openAssemblyStream: () => currentPEStream = new MemoryStream([.. peImage]), openPdbStream: () => { if (pdbStream == null) diff --git a/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs b/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs index 793df59acf65f..00ceef681f06d 100644 --- a/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs +++ b/src/EditorFeatures/Test/Structure/StructureTaggerTests.cs @@ -339,7 +339,7 @@ private static async Task> GetTagsFromWorkspaceAsyn var context = new TaggerContext(document, view.TextSnapshot, frozenPartialSemantics: false); await provider.GetTestAccessor().ProduceTagsAsync(context); - return context.TagSpans.Select(x => x.Tag).OrderBy(t => t.OutliningSpan.Value.Start).ToList(); + return [.. context.TagSpans.Select(x => x.Tag).OrderBy(t => t.OutliningSpan.Value.Start)]; } #pragma warning restore CS0618 // Type or member is obsolete diff --git a/src/EditorFeatures/Test/Utilities/AsynchronousOperationListenerTests.cs b/src/EditorFeatures/Test/Utilities/AsynchronousOperationListenerTests.cs index 09da0aa6cb580..c364f5a0ffd40 100644 --- a/src/EditorFeatures/Test/Utilities/AsynchronousOperationListenerTests.cs +++ b/src/EditorFeatures/Test/Utilities/AsynchronousOperationListenerTests.cs @@ -35,7 +35,7 @@ public void Dispose() lock (_tasks) { _tokenSource.Cancel(); - tasks = _tasks.ToArray(); + tasks = [.. _tasks]; } try diff --git a/src/EditorFeatures/Test/Utilities/BloomFilterTests.cs b/src/EditorFeatures/Test/Utilities/BloomFilterTests.cs index d8fd33a73fc2c..33bff5542e174 100644 --- a/src/EditorFeatures/Test/Utilities/BloomFilterTests.cs +++ b/src/EditorFeatures/Test/Utilities/BloomFilterTests.cs @@ -41,7 +41,8 @@ private static string GenerateString(int value) return builder.ToString(); } - private static void Test(bool isCaseSensitive) + [Theory, CombinatorialData] + public void Test(bool isCaseSensitive) { var comparer = isCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; var strings = new HashSet(GenerateStrings(2000).Skip(500).Take(1000), comparer); @@ -79,14 +80,6 @@ private static void Test(bool isCaseSensitive) } } - [Fact] - public void Test1() - => Test(isCaseSensitive: true); - - [Fact] - public void TestInsensitive() - => Test(isCaseSensitive: false); - [Fact] public void TestEmpty() { @@ -106,6 +99,33 @@ public void TestEmpty() } } + [Fact] + public void TestCacheWhenEmpty() + { + BloomFilter.BloomFilterHash.ResetCachedEntry(); + + _ = new BloomFilter(falsePositiveProbability: 0.0001, isCaseSensitive: false, []); + + Assert.False(BloomFilter.BloomFilterHash.TryGetCachedEntry(out _, out _)); + } + + [Fact] + public void TestCacheAfterCalls() + { + var filter1 = new BloomFilter(falsePositiveProbability: 0.0001, isCaseSensitive: false, []); + var filter2 = new BloomFilter(falsePositiveProbability: 0.0001, isCaseSensitive: true, []); + + _ = filter1.ProbablyContains("test1"); + Assert.True(BloomFilter.BloomFilterHash.TryGetCachedEntry(out var isCaseSensitive, out var value)); + Assert.True(!isCaseSensitive); + Assert.Equal("test1", value); + + _ = filter2.ProbablyContains("test2"); + Assert.True(BloomFilter.BloomFilterHash.TryGetCachedEntry(out isCaseSensitive, out value)); + Assert.True(isCaseSensitive); + Assert.Equal("test2", value); + } + [Fact] public void TestSerialization() { @@ -180,6 +200,78 @@ public void TestInt64() } } + [Theory, CombinatorialData] + public void TestCacheCorrectness(bool isCaseSensitive, bool reverse) + { + var allStringsToTest = GenerateStrings(100_000); + + var comparer = isCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; + var allHashSets = new List> + { + new HashSet(GenerateStrings(1_000), comparer), + new HashSet(GenerateStrings(1_000).Where((s, i) => i % 1 == 0), comparer), + new HashSet(GenerateStrings(1_000).Where((s, i) => i % 1 == 1), comparer), + new HashSet(GenerateStrings(10_000), comparer), + new HashSet(GenerateStrings(10_000).Where((s, i) => i % 1 == 0), comparer), + new HashSet(GenerateStrings(10_000).Where((s, i) => i % 1 == 1), comparer), + new HashSet(GenerateStrings(100_000), comparer), + new HashSet(GenerateStrings(100_000).Where((s, i) => i % 1 == 0), comparer), + new HashSet(GenerateStrings(100_000).Where((s, i) => i % 1 == 1), comparer), + }; + + // Try the patterns where we're searching smaller filters then larger ones. Then the pattern of larger ones then smaller ones. + if (reverse) + allHashSets.Reverse(); + + // Try several different probability levels to ensure we maintain the correct false positive rate. We + // must always preserve the true 0 negative rate. + for (var d = 0.1; d >= 0.0001; d /= 10) + { + // Get a bloom filter for each set of strings. + var allFilters = allHashSets.Select(s => new BloomFilter(d, isCaseSensitive, s)).ToArray(); + + // The double array stores the correct/incorrect count per run. + var allCounts = allHashSets.Select(_ => new double[2]).ToArray(); + + // We want to take each string, and test it against each bloom filter. This will ensure that the caches + // we have when computing against one bloom filter don't infect the results of the other bloom filters. + foreach (var test in allStringsToTest) + { + for (var i = 0; i < allHashSets.Count; i++) + { + var strings = allHashSets[i]; + var filter = allFilters[i]; + var counts = allCounts[i]; + var actualContains = strings.Contains(test); + var filterContains = filter.ProbablyContains(test); + + // if the filter says no, then it can't be in the real set. + if (!filterContains) + Assert.False(actualContains); + + if (actualContains == filterContains) + { + counts[0]++; + } + else + { + counts[1]++; + } + } + } + + // Now validate for this set of bloom filters, and this particular probability level, that all the + // rates remain correct for each bloom filter. + foreach (var counts in allCounts) + { + var correctCount = counts[0]; + var incorrectCount = counts[1]; + var falsePositivePercentage = incorrectCount / (correctCount + incorrectCount); + Assert.True(falsePositivePercentage < (d * 1.5), string.Format("falsePositivePercentage={0}, d={1}", falsePositivePercentage, d)); + } + } + } + private static HashSet CreateLongs(List ints) { var result = new HashSet(); diff --git a/src/EditorFeatures/Test/Workspaces/TextFactoryTests.cs b/src/EditorFeatures/Test/Workspaces/TextFactoryTests.cs index 6ebfbc1b253d4..377d303f6a210 100644 --- a/src/EditorFeatures/Test/Workspaces/TextFactoryTests.cs +++ b/src/EditorFeatures/Test/Workspaces/TextFactoryTests.cs @@ -68,7 +68,10 @@ public void TestCreateTextUsesByteOrderMarkIfPresent() TestCreateTextInferredEncoding( textFactoryService, - Encoding.UTF8.GetPreamble().Concat(new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetBytes("Test")).ToArray(), + [ + .. Encoding.UTF8.GetPreamble(), + .. new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true).GetBytes("Test"), + ], defaultEncoding: Encoding.GetEncoding(1254), expectedEncoding: Encoding.UTF8); } @@ -82,13 +85,11 @@ public async Task TestCreateFromTemporaryStorage() var text = SourceText.From("Hello, World!"); - // Create a temporary storage location - using var temporaryStorage = temporaryStorageService.CreateTemporaryTextStorage(); // Write text into it - await temporaryStorage.WriteTextAsync(text); + var handle = await temporaryStorageService.WriteToTemporaryStorageAsync(text, CancellationToken.None); // Read text back from it - var text2 = await temporaryStorage.ReadTextAsync(); + var text2 = await handle.ReadFromTemporaryStorageAsync(CancellationToken.None); Assert.NotSame(text, text2); Assert.Equal(text.ToString(), text2.ToString()); @@ -104,13 +105,11 @@ public async Task TestCreateFromTemporaryStorageWithEncoding() var text = SourceText.From("Hello, World!", Encoding.ASCII); - // Create a temporary storage location - using var temporaryStorage = temporaryStorageService.CreateTemporaryTextStorage(); // Write text into it - await temporaryStorage.WriteTextAsync(text); + var handle = await temporaryStorageService.WriteToTemporaryStorageAsync(text, CancellationToken.None); // Read text back from it - var text2 = await temporaryStorage.ReadTextAsync(); + var text2 = await handle.ReadFromTemporaryStorageAsync(CancellationToken.None); Assert.NotSame(text, text2); Assert.Equal(text.ToString(), text2.ToString()); diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticProviderTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticProviderTests.vb index 479f3b4014a6b..19a9f764f34be 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticProviderTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticProviderTests.vb @@ -261,11 +261,9 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests Next Dim diagnosticProvider = GetDiagnosticProvider(workspace) - Dim actualDiagnostics = diagnosticProvider.GetDiagnosticsAsync( - workspace.CurrentSolution, projectId:=Nothing, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None).Result + Dim actualDiagnostics = diagnosticProvider.GetDiagnosticsForIdsAsync( + workspace.CurrentSolution, projectId:=Nothing, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None).Result If diagnostics Is Nothing Then Assert.Equal(0, actualDiagnostics.Length) diff --git a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb index 1f9aec9da2ced..d6722a2eee3cf 100644 --- a/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb +++ b/src/EditorFeatures/Test2/Diagnostics/DiagnosticServiceTests.vb @@ -536,10 +536,9 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests Dim diagnostics = Await GetDiagnosticsForSpanAsync(diagnosticService, document, root.FullSpan) Assert.Equal(0, diagnostics.Count()) - diagnostics = Await diagnosticService.GetDiagnosticsAsync(project.Solution, projectId:=Nothing, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None).ConfigureAwait(False) + diagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + project.Solution, projectId:=Nothing, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Dim diagnostic = diagnostics.First() Assert.True(diagnostic.Id = "AD0001") Assert.Contains("CodeBlockStartedAnalyzer", diagnostic.Message, StringComparison.Ordinal) @@ -608,10 +607,9 @@ Namespace Microsoft.CodeAnalysis.Editor.Implementation.Diagnostics.UnitTests Assert.Equal(1, descriptorsMap.Count) Dim document = project.Documents.Single() - Dim diagnostics = Await diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None) + Dim diagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Assert.Equal(1, diagnostics.Count()) Dim diagnostic = diagnostics.First() Assert.Equal(OperationAnalyzer.Descriptor.Id, diagnostic.Id) @@ -951,10 +949,9 @@ class AnonymousFunctions ' Verify no duplicate analysis/diagnostics. Dim document = project.Documents.Single() - Dim diagnostics = (Await diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None)). + Dim diagnostics = (Await diagnosticService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None)). Select(Function(d) d.Id = NamedTypeAnalyzer.DiagDescriptor.Id) Assert.Equal(1, diagnostics.Count) @@ -1047,10 +1044,9 @@ class AnonymousFunctions ' Verify project diagnostics contains diagnostics reported on both partial definitions. Dim incrementalAnalyzer = diagnosticService.CreateIncrementalAnalyzer(workspace) - Dim diagnostics = Await diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None) + Dim diagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Assert.Equal(2, diagnostics.Count()) Dim file1HasDiag = False, file2HasDiag = False For Each diagnostic In diagnostics @@ -2151,10 +2147,9 @@ class MyClass Assert.Equal(expectedCount, diagnostics.Count()) ' Get diagnostics explicitly - Dim hiddenDiagnostics = Await diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None) + Dim hiddenDiagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Assert.Equal(1, hiddenDiagnostics.Count()) Assert.Equal(analyzer.Descriptor.Id, hiddenDiagnostics.Single().Id) End Using @@ -2239,10 +2234,9 @@ class C Assert.Equal(1, descriptorsMap.Count) Dim incrementalAnalyzer = diagnosticService.CreateIncrementalAnalyzer(workspace) - Dim diagnostics = Await diagnosticService.GetDiagnosticsAsync(project.Solution, project.Id, documentId:=Nothing, - includeSuppressedDiagnostics:=False, - includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None) + Dim diagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId:=Nothing, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Assert.Equal(0, diagnostics.Count()) End Using End Function diff --git a/src/EditorFeatures/TestUtilities/Diagnostics/MockDiagnosticAnalyzerService.cs b/src/EditorFeatures/TestUtilities/Diagnostics/MockDiagnosticAnalyzerService.cs index 6bd490efea2e5..424a6876218c1 100644 --- a/src/EditorFeatures/TestUtilities/Diagnostics/MockDiagnosticAnalyzerService.cs +++ b/src/EditorFeatures/TestUtilities/Diagnostics/MockDiagnosticAnalyzerService.cs @@ -61,7 +61,7 @@ public Task> GetCachedDiagnosticsAsync(Workspace public Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) + public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task> GetDiagnosticsForSpanAsync(TextDocument document, TextSpan? range, Func? shouldIncludeDiagnostic, bool includeCompilerDiagnostics, bool includeSuppressedDiagnostics, ICodeActionRequestPriorityProvider priorityProvider, Func? addOperationScope, DiagnosticKind diagnosticKind, bool isExplicit, CancellationToken cancellationToken) diff --git a/src/EditorFeatures/TestUtilities/NavigateTo/FirstDocIsActiveAndVisibleDocumentTrackingService.cs b/src/EditorFeatures/TestUtilities/DocumentTracking/FirstDocumentIsActiveAndVisibleDocumentTrackingService.cs similarity index 72% rename from src/EditorFeatures/TestUtilities/NavigateTo/FirstDocIsActiveAndVisibleDocumentTrackingService.cs rename to src/EditorFeatures/TestUtilities/DocumentTracking/FirstDocumentIsActiveAndVisibleDocumentTrackingService.cs index 7a02c1defd780..4f8ccd9d7cdc6 100644 --- a/src/EditorFeatures/TestUtilities/NavigateTo/FirstDocIsActiveAndVisibleDocumentTrackingService.cs +++ b/src/EditorFeatures/TestUtilities/DocumentTracking/FirstDocumentIsActiveAndVisibleDocumentTrackingService.cs @@ -10,25 +10,23 @@ using Microsoft.CodeAnalysis.Host.Mef; using Roslyn.Utilities; -namespace Microsoft.CodeAnalysis.Editor.UnitTests.NavigateTo; +namespace Microsoft.CodeAnalysis.Editor.Test; -internal sealed class FirstDocIsActiveAndVisibleDocumentTrackingService : IDocumentTrackingService +internal sealed class FirstDocumentIsActiveAndVisibleDocumentTrackingService : IDocumentTrackingService { private readonly Workspace _workspace; [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] - private FirstDocIsActiveAndVisibleDocumentTrackingService(Workspace workspace) + private FirstDocumentIsActiveAndVisibleDocumentTrackingService(Workspace workspace) => _workspace = workspace; - public bool SupportsDocumentTracking => true; - public event EventHandler ActiveDocumentChanged { add { } remove { } } public DocumentId TryGetActiveDocument() => _workspace.CurrentSolution.Projects.First().DocumentIds.First(); public ImmutableArray GetVisibleDocuments() - => ImmutableArray.Create(_workspace.CurrentSolution.Projects.First().DocumentIds.First()); + => [TryGetActiveDocument()]; [ExportWorkspaceServiceFactory(typeof(IDocumentTrackingService), ServiceLayer.Test), Shared, PartNotDiscoverable] public class Factory : IWorkspaceServiceFactory @@ -41,6 +39,6 @@ public Factory() [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new FirstDocIsActiveAndVisibleDocumentTrackingService(workspaceServices.Workspace); + => new FirstDocumentIsActiveAndVisibleDocumentTrackingService(workspaceServices.Workspace); } } diff --git a/src/EditorFeatures/TestUtilities/DocumentTracking/TestDocumentTrackingService.cs b/src/EditorFeatures/TestUtilities/DocumentTracking/TestDocumentTrackingService.cs new file mode 100644 index 0000000000000..ec01c8c69affc --- /dev/null +++ b/src/EditorFeatures/TestUtilities/DocumentTracking/TestDocumentTrackingService.cs @@ -0,0 +1,32 @@ +// 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.Host.Mef; + +namespace Microsoft.CodeAnalysis.Editor.Test; + +[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Test), Shared, PartNotDiscoverable] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class TestDocumentTrackingService() : IDocumentTrackingService +{ + private DocumentId? _activeDocumentId; + + public event EventHandler? ActiveDocumentChanged; + + public void SetActiveDocument(DocumentId? newActiveDocumentId) + { + _activeDocumentId = newActiveDocumentId; + ActiveDocumentChanged?.Invoke(this, newActiveDocumentId); + } + + public DocumentId? TryGetActiveDocument() + => _activeDocumentId; + + public ImmutableArray GetVisibleDocuments() + => _activeDocumentId != null ? [_activeDocumentId] : []; +} diff --git a/src/EditorFeatures/TestUtilities/NavigateTo/AbstractNavigateToTests.cs b/src/EditorFeatures/TestUtilities/NavigateTo/AbstractNavigateToTests.cs index 619033a76de5a..22b76a49786d6 100644 --- a/src/EditorFeatures/TestUtilities/NavigateTo/AbstractNavigateToTests.cs +++ b/src/EditorFeatures/TestUtilities/NavigateTo/AbstractNavigateToTests.cs @@ -17,6 +17,7 @@ using Microsoft.CodeAnalysis.Editor.Implementation.NavigateTo; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Editor.Wpf; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; @@ -41,7 +42,7 @@ public abstract partial class AbstractNavigateToTests { protected static readonly TestComposition DefaultComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(TestWorkspaceNavigateToSearchHostService)); protected static readonly TestComposition FirstVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(TestWorkspaceNavigateToSearchHostService), typeof(FirstDocIsVisibleDocumentTrackingService.Factory)); - protected static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(TestWorkspaceNavigateToSearchHostService), typeof(FirstDocIsActiveAndVisibleDocumentTrackingService.Factory)); + protected static readonly TestComposition FirstActiveAndVisibleComposition = EditorTestCompositions.EditorFeatures.AddParts(typeof(TestWorkspaceNavigateToSearchHostService), typeof(FirstDocumentIsActiveAndVisibleDocumentTrackingService.Factory)); protected INavigateToItemProvider _provider; protected NavigateToTestAggregator _aggregator; @@ -162,7 +163,7 @@ internal void InitializeWorkspace(EditorTestWorkspace workspace) protected static void VerifyNavigateToResultItems( List expecteditems, IEnumerable items) { - expecteditems = expecteditems.OrderBy(i => i.Name).ToList(); + expecteditems = [.. expecteditems.OrderBy(i => i.Name)]; items = items.OrderBy(i => i.Name).ToList(); Assert.Equal(expecteditems.Count(), items.Count()); @@ -237,8 +238,6 @@ private class FirstDocIsVisibleDocumentTrackingService : IDocumentTrackingServic private FirstDocIsVisibleDocumentTrackingService(Workspace workspace) => _workspace = workspace; - public bool SupportsDocumentTracking => true; - public event EventHandler ActiveDocumentChanged { add { } remove { } } public DocumentId TryGetActiveDocument() diff --git a/src/EditorFeatures/TestUtilities/SolutionCrawler/TestDocumentTrackingService.cs b/src/EditorFeatures/TestUtilities/SolutionCrawler/TestDocumentTrackingService.cs deleted file mode 100644 index de2466aa68453..0000000000000 --- a/src/EditorFeatures/TestUtilities/SolutionCrawler/TestDocumentTrackingService.cs +++ /dev/null @@ -1,39 +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.Host.Mef; - -namespace Microsoft.CodeAnalysis.Editor.Test -{ - [ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Test), Shared, PartNotDiscoverable] - internal sealed class TestDocumentTrackingService : IDocumentTrackingService - { - private DocumentId? _activeDocumentId; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TestDocumentTrackingService() - { - } - - public bool SupportsDocumentTracking => true; - - public event EventHandler? ActiveDocumentChanged; - - public void SetActiveDocument(DocumentId? newActiveDocumentId) - { - _activeDocumentId = newActiveDocumentId; - ActiveDocumentChanged?.Invoke(this, newActiveDocumentId); - } - - public DocumentId? TryGetActiveDocument() - => _activeDocumentId; - - public ImmutableArray GetVisibleDocuments() - => _activeDocumentId != null ? ImmutableArray.Create(_activeDocumentId) : ImmutableArray.Empty; - } -} diff --git a/src/EditorFeatures/TestUtilities/Squiggles/SquiggleUtilities.cs b/src/EditorFeatures/TestUtilities/Squiggles/SquiggleUtilities.cs index 1a76d41a4f352..90ed3e6ea22e0 100644 --- a/src/EditorFeatures/TestUtilities/Squiggles/SquiggleUtilities.cs +++ b/src/EditorFeatures/TestUtilities/Squiggles/SquiggleUtilities.cs @@ -28,7 +28,7 @@ public static class SquiggleUtilities internal static TestComposition WpfCompositionWithSolutionCrawler = EditorTestCompositions.EditorFeaturesWpf .RemoveParts(typeof(MockWorkspaceEventListenerProvider)); - internal static async Task<(ImmutableArray, ImmutableArray>)> GetDiagnosticsAndErrorSpansAsync( + internal static async Task>> GetTagSpansAsync( EditorTestWorkspace workspace, IReadOnlyDictionary> analyzerMap = null) where TProvider : AbstractDiagnosticsTaggerProvider @@ -43,14 +43,10 @@ public static class SquiggleUtilities using var disposable = tagger as IDisposable; await wrapper.WaitForTags(); - var service = (DiagnosticAnalyzerService)workspace.ExportProvider.GetExportedValue(); - var analyzerDiagnostics = await service.GetDiagnosticsAsync(workspace.CurrentSolution, - projectId: null, documentId: null, includeSuppressedDiagnostics: false, includeNonLocalDocumentDiagnostics: true, CancellationToken.None); - var snapshot = textBuffer.CurrentSnapshot; var spans = tagger.GetTags(snapshot.GetSnapshotSpanCollection()).ToImmutableArray(); - return (analyzerDiagnostics, spans); + return spans; } } } diff --git a/src/EditorFeatures/TestUtilities/Squiggles/TestDiagnosticTagProducer.cs b/src/EditorFeatures/TestUtilities/Squiggles/TestDiagnosticTagProducer.cs index 79108121d640b..d42a8b5abcc4d 100644 --- a/src/EditorFeatures/TestUtilities/Squiggles/TestDiagnosticTagProducer.cs +++ b/src/EditorFeatures/TestUtilities/Squiggles/TestDiagnosticTagProducer.cs @@ -18,11 +18,11 @@ internal sealed class TestDiagnosticTagProducer where TProvider : AbstractDiagnosticsTaggerProvider where TTag : class, ITag { - internal static Task<(ImmutableArray, ImmutableArray>)> GetDiagnosticsAndErrorSpans( + internal static Task>> GetTagSpansAsync( EditorTestWorkspace workspace, IReadOnlyDictionary>? analyzerMap = null) { - return SquiggleUtilities.GetDiagnosticsAndErrorSpansAsync(workspace, analyzerMap); + return SquiggleUtilities.GetTagSpansAsync(workspace, analyzerMap); } internal static DiagnosticData CreateDiagnosticData(EditorTestHostDocument document, TextSpan span) diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs index e6f199978569a..60ebe77ff745b 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/DeclarationName/DeclarationNameCompletionProvider.cs @@ -30,7 +30,7 @@ namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers; [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)]; internal override string Language => LanguageNames.CSharp; diff --git a/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs b/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs index edfc5c4e75f64..ea9130d85ec3a 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/CSharpConvertLinqQueryToForEachProvider.cs @@ -827,7 +827,7 @@ private StatementSyntax[] GenerateStatements( // 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(); + return [.. statements]; } private bool TryProcessQueryBody(QueryBodySyntax queryBody, QueryExpressionProcessingInfo queryExpressionProcessingInfo) diff --git a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs index ea8ae8a8da756..61f9cf84c86fe 100644 --- a/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs +++ b/src/Features/CSharp/Portable/ConvertLinq/ConvertForEachToLinqQuery/AbstractToMethodConverter.cs @@ -124,8 +124,7 @@ void Convert(ExpressionSyntax replacingExpression, SyntaxNode nodeToRemoveIfFoll // Output: // return queryGenerated.ToList(); or return queryGenerated.Count(); replacingExpression = returnStatement.Expression; - leadingTrivia = GetTriviaFromNode(nodeToRemoveIfFollowedByReturn) - .Concat(SyntaxNodeOrTokenExtensions.GetTrivia(replacingExpression)).ToArray(); + leadingTrivia = [.. GetTriviaFromNode(nodeToRemoveIfFollowedByReturn), .. SyntaxNodeOrTokenExtensions.GetTrivia(replacingExpression)]; editor.RemoveNode(nodeToRemoveIfFollowedByReturn); } else diff --git a/src/Features/CSharp/Portable/ConvertPrimaryToRegularConstructor/ConvertPrimaryToRegularConstructorCodeRefactoringProvider.cs b/src/Features/CSharp/Portable/ConvertPrimaryToRegularConstructor/ConvertPrimaryToRegularConstructorCodeRefactoringProvider.cs index 6cedb5812a4f7..617ca5843708c 100644 --- a/src/Features/CSharp/Portable/ConvertPrimaryToRegularConstructor/ConvertPrimaryToRegularConstructorCodeRefactoringProvider.cs +++ b/src/Features/CSharp/Portable/ConvertPrimaryToRegularConstructor/ConvertPrimaryToRegularConstructorCodeRefactoringProvider.cs @@ -240,7 +240,7 @@ ImmutableDictionary CreateSynthesizedFields() } } - return result.ToImmutableHashSet(); + return [.. result]; } void RemovePrimaryConstructorParameterList() diff --git a/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs b/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs index c8a48212c6e61..e472dfb91502e 100644 --- a/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs +++ b/src/Features/CSharp/Portable/DocumentHighlighting/CSharpDocumentHighlightsService.cs @@ -24,10 +24,12 @@ namespace Microsoft.CodeAnalysis.CSharp.DocumentHighlighting; [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] internal class CSharpDocumentHighlightsService( - [ImportMany] IEnumerable> services) : AbstractDocumentHighlightsService(LanguageNames.CSharp, - CSharpEmbeddedLanguagesProvider.Info, - CSharpSyntaxKinds.Instance, - services) + [ImportMany] IEnumerable> services) + : AbstractDocumentHighlightsService( + LanguageNames.CSharp, + CSharpEmbeddedLanguagesProvider.Info, + CSharpSyntaxKinds.Instance, + services) { protected override async Task> GetAdditionalReferencesAsync( Document document, ISymbol symbol, CancellationToken cancellationToken) @@ -56,7 +58,9 @@ protected override async Task> GetAdditionalReferencesA if (type.IsVar) { - semanticModel ??= await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + // Document highlights are not impacted by nullable analysis. Get a semantic model with nullability + // disabled to lower the amount of work we need to do here. + semanticModel ??= await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false); var boundSymbol = semanticModel.GetSymbolInfo(type, cancellationToken).Symbol; boundSymbol = boundSymbol?.OriginalDefinition; diff --git a/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs b/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs index dddf059963c97..991287eeaeeaf 100644 --- a/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs +++ b/src/Features/CSharp/Portable/EditAndContinue/CSharpEditAndContinueAnalyzer.cs @@ -921,7 +921,7 @@ private static bool AreSimilarActiveStatements(CommonForEachStatementSyntax oldN RoslynDebug.Assert(oldTokens != null); RoslynDebug.Assert(newTokens != null); - return DeclareSameIdentifiers(oldTokens.ToArray(), newTokens.ToArray()); + return DeclareSameIdentifiers([.. oldTokens], [.. newTokens]); } protected override bool AreEquivalentImpl(SyntaxToken oldToken, SyntaxToken newToken) diff --git a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs index 6bab88b6f96d8..3a5e3e9a9d8dd 100644 --- a/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs +++ b/src/Features/CSharp/Portable/GenerateMember/GenerateParameterizedMember/CSharpGenerateParameterizedMemberService.cs @@ -59,7 +59,7 @@ protected override ImmutableArray GetCapturedTypeParameter type.GetReferencedTypeParameters(result); } - return result.ToImmutableArray(); + return [.. result]; } protected override ImmutableArray GenerateTypeParameters(CancellationToken cancellationToken) diff --git a/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs b/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs index 2c06e936fc06a..3e439cc5f6b4e 100644 --- a/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs +++ b/src/Features/CSharp/Portable/QuickInfo/CSharpDiagnosticAnalyzerQuickInfoProvider.cs @@ -156,15 +156,15 @@ private static QuickInfoItem CreateQuickInfo(TextSpan location, DiagnosticDescri 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[] - { + return QuickInfoItem.Create(location, sections: + [ + QuickInfoSection.Create(QuickInfoSectionKinds.Description, + [ idTag, new TaggedText(TextTags.Punctuation, ":"), new TaggedText(TextTags.Space, " "), new TaggedText(TextTags.Text, description) - }.ToImmutableArray()) - }.ToImmutableArray(), relatedSpans: relatedSpans.ToImmutableArray()); + ]) + ], relatedSpans: [.. relatedSpans]); } } diff --git a/src/Features/CSharpTest/EditAndContinue/Helpers/EditAndContinueValidation.cs b/src/Features/CSharpTest/EditAndContinue/Helpers/EditAndContinueValidation.cs index 7d9e4f7c66ec5..9e42352af2e27 100644 --- a/src/Features/CSharpTest/EditAndContinue/Helpers/EditAndContinueValidation.cs +++ b/src/Features/CSharpTest/EditAndContinue/Helpers/EditAndContinueValidation.cs @@ -27,7 +27,7 @@ internal static void VerifyLineEdits( VerifyLineEdits( editScript, - new[] { new SequencePointUpdates(editScript.Match.OldRoot.SyntaxTree.FilePath, lineEdits.ToImmutableArray()) }, + new[] { new SequencePointUpdates(editScript.Match.OldRoot.SyntaxTree.FilePath, [.. lineEdits]) }, semanticEdits, diagnostics, capabilities); diff --git a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs index 1eac532fdd3bb..7967d1f73cb8a 100644 --- a/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs +++ b/src/Features/CSharpTest/EditAndContinue/TopLevelEditingTests.cs @@ -3679,7 +3679,7 @@ public void Record_Property_Delete_ReplacingCustomWithSynthesized_WithBodyAndMet expectedEdits.Add(SemanticEdit(SemanticEditKind.Update, c => c.GetPrimaryConstructor("C"), preserveLocalVariables: true)); edits.VerifySemantics( - expectedEdits.ToArray(), + [.. expectedEdits], capabilities: EditAndContinueCapabilities.AddInstanceFieldToExistingType); } diff --git a/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs b/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs index ca3f102bcfc9a..84a7d0ffe9b66 100644 --- a/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs +++ b/src/Features/Core/Portable/AddImport/AbstractAddImportFeatureService.cs @@ -192,7 +192,7 @@ await FindResultsInAllSymbolsInStartingProjectAsync( } } - return allReferences.ToImmutableArray(); + return [.. allReferences]; } private static async Task FindResultsInAllSymbolsInStartingProjectAsync( diff --git a/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs b/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs index 4850d08d7fd1b..945aa8594c7ff 100644 --- a/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs +++ b/src/Features/Core/Portable/AddImport/SymbolReferenceFinder.cs @@ -138,12 +138,11 @@ private async Task> DoAsync(SearchScope searchSc private ImmutableArray DeDupeAndSortReferences(ImmutableArray allReferences) { - return allReferences + return [.. allReferences .Distinct() .Where(NotNull) .Where(NotGlobalNamespace) - .OrderBy((r1, r2) => r1.CompareTo(_document, r2)) - .ToImmutableArray(); + .OrderBy((r1, r2) => r1.CompareTo(_document, r2))]; } private static void CalculateContext( diff --git a/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs b/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs index 023fb637ee227..fab481046098c 100644 --- a/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.cs +++ b/src/Features/Core/Portable/ChangeSignature/DelegateInvokeMethodReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Linq; @@ -59,20 +60,27 @@ protected override async ValueTask> DetermineCascadedSym return result.ToImmutableAndClear(); } - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return Task.FromResult(project.Documents.ToImmutableArray()); + foreach (var document in project.Documents) + processResult(document, processResultData); + + return Task.CompletedTask; } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -83,7 +91,12 @@ protected override async ValueTask> FindReference var root = state.Root; var nodes = root.DescendantNodes(); - using var _ = ArrayBuilder.GetInstance(out var convertedAnonymousFunctions); + var invocations = nodes.Where(syntaxFacts.IsInvocationExpression) + .Where(e => state.SemanticModel.GetSymbolInfo(e, cancellationToken).Symbol?.OriginalDefinition == methodSymbol); + + foreach (var node in invocations) + processResult(CreateFinderLocation(node, state, cancellationToken), processResultData); + foreach (var node in nodes) { if (!syntaxFacts.IsAnonymousFunctionExpression(node)) @@ -97,14 +110,17 @@ protected override async ValueTask> FindReference } if (convertedType == methodSymbol.ContainingType) - convertedAnonymousFunctions.Add(node); + { + var finderLocation = CreateFinderLocation(node, state, cancellationToken); + processResult(finderLocation, processResultData); + } } - var invocations = nodes.Where(syntaxFacts.IsInvocationExpression) - .Where(e => state.SemanticModel.GetSymbolInfo(e, cancellationToken).Symbol?.OriginalDefinition == methodSymbol); + return; - return invocations.Concat(convertedAnonymousFunctions).SelectAsArray( - node => new FinderLocation( + static FinderLocation CreateFinderLocation(SyntaxNode node, FindReferencesDocumentState state, CancellationToken cancellationToken) + { + return new FinderLocation( node, new ReferenceLocation( state.Document, @@ -113,6 +129,7 @@ protected override async ValueTask> FindReference isImplicit: false, GetSymbolUsageInfo(node, state, cancellationToken), GetAdditionalFindUsagesProperties(node, state), - CandidateReason.None))); + CandidateReason.None)); + } } } diff --git a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs index a9d1b96663ade..2d332d593df81 100644 --- a/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs +++ b/src/Features/Core/Portable/CodeFixes/Configuration/ConfigurationUpdater.cs @@ -385,10 +385,10 @@ internal static ImmutableArray GetCodeStyleOptionsForDiagnostic(Diagno { 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]; } return []; diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs index fd9d7e160d41c..5b0a56aaff0be 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionBatchFixAllProvider.cs @@ -110,7 +110,7 @@ internal abstract class AbstractSuppressionBatchFixAllProvider : FixAllProvider await Task.WhenAll(tasks).ConfigureAwait(false); } - return fixesBag.ToImmutableArray(); + return [.. fixesBag]; } private async Task AddDocumentFixesAsync( diff --git a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs index c748295d4eabb..e0c7a073532e0 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.PragmaHelpers.cs @@ -164,11 +164,11 @@ internal static SyntaxToken GetNewEndTokenWithAddedPragma( var isEOF = fixer.IsEndOfFileToken(endToken); if (isEOF) { - trivia = endToken.LeadingTrivia.ToImmutableArray(); + trivia = [.. endToken.LeadingTrivia]; } else { - trivia = endToken.TrailingTrivia.ToImmutableArray(); + trivia = [.. endToken.TrailingTrivia]; } var index = GetPositionForPragmaInsertion(trivia, currentDiagnosticSpan, fixer, isStartToken: false, triviaAtIndex: out var insertBeforeTrivia); 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 44d0ae9e5e5a5..a52525a81854c 100644 --- a/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.BatchFixer.cs +++ b/src/Features/Core/Portable/CodeFixes/Suppression/AbstractSuppressionCodeFixProvider.RemoveSuppressionCodeAction.BatchFixer.cs @@ -153,7 +153,7 @@ public override async Task TryGetMergedFixAsync( } return await base.TryGetMergedFixAsync( - newBatchOfFixes.ToImmutableArray(), fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); + [.. newBatchOfFixes], fixAllState, progressTracker, cancellationToken).ConfigureAwait(false); } private static async Task> GetAttributeNodesToFixAsync(ImmutableArray attributeRemoveFixes, CancellationToken cancellationToken) diff --git a/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs b/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs index 076e4ddaabbdb..5a0a535ac915c 100644 --- a/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs +++ b/src/Features/Core/Portable/CodeLens/CodeLensFindReferenceProgress.cs @@ -48,7 +48,7 @@ internal sealed class CodeLensFindReferencesProgress( public int ReferencesCount => _locations.Count; - public ImmutableArray Locations => _locations.ToImmutableArray(); + public ImmutableArray Locations => [.. _locations]; public void OnStarted() { diff --git a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs index d0326a24686fe..49dd3e0320e36 100644 --- a/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/CodeRefactoringService.cs @@ -212,7 +212,7 @@ private class ProjectCodeRefactoringProvider : AbstractProjectExtensionProvider { protected override ImmutableArray GetLanguages(ExportCodeRefactoringProviderAttribute exportAttribute) - => exportAttribute.Languages.ToImmutableArray(); + => [.. exportAttribute.Languages]; protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) { diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs index a67552e29dedd..b9dd43e57681d 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractChangeNamespaceService.cs @@ -259,7 +259,7 @@ await ChangeNamespaceInSingleDocumentAsync(solutionAfterNamespaceChange, documen solutionAfterImportsRemoved = await RemoveUnnecessaryImportsAsync( solutionAfterImportsRemoved, - referenceDocuments.ToImmutableArray(), + [.. referenceDocuments], [declaredNamespace, targetNamespace], fallbackOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs index 9f9afa686ebeb..5a7dbf9fb95b9 100644 --- a/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs +++ b/src/Features/Core/Portable/CodeRefactorings/SyncNamespace/AbstractSyncNamespaceCodeRefactoringProvider.State.cs @@ -158,7 +158,7 @@ private static bool IsDocumentPathRootedInProjectFolder(Document document) if (projectRoot is null) return false; - var folderPath = Path.Combine(document.Folders.ToArray()); + var folderPath = Path.Combine([.. document.Folders]); var logicalDirectoryPath = PathUtilities.CombineAbsoluteAndRelativePaths(projectRoot, folderPath); if (logicalDirectoryPath is null) return false; diff --git a/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs b/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs index 9115fa8976a73..b05169033da52 100644 --- a/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs +++ b/src/Features/Core/Portable/Completion/CharacterSetModificationRule.cs @@ -43,5 +43,5 @@ public static CharacterSetModificationRule Create(CharacterSetModificationKind k /// One or more characters. These are typically punctuation characters. /// public static CharacterSetModificationRule Create(CharacterSetModificationKind kind, params char[] characters) - => new(kind, characters.ToImmutableArray()); + => new(kind, [.. characters]); } diff --git a/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs b/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs index 9c707d7f8cb70..5095803afa12f 100644 --- a/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs +++ b/src/Features/Core/Portable/Completion/CompletionService_GetCompletions.cs @@ -137,13 +137,13 @@ ImmutableArray GetTriggeredProviders( 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; + return triggeredProviders.IsEmpty ? [.. providers] : triggeredProviders; } return []; default: - return providers.ToImmutableArray(); + return [.. providers]; } } diff --git a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs index ace71a736df3c..8f7035e04710f 100644 --- a/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs +++ b/src/Features/Core/Portable/Completion/Providers/AbstractSymbolCompletionProvider.cs @@ -277,7 +277,7 @@ private async Task> GetItemsAsync( var totalProjects = contextAndSymbolLists.Select(t => t.documentId.ProjectId).ToList(); return CreateItems( - completionContext, symbolToContextMap.Keys.ToImmutableArray(), symbol => symbolToContextMap[symbol], missingSymbolsMap, totalProjects); + completionContext, [.. symbolToContextMap.Keys], symbol => symbolToContextMap[symbol], missingSymbolsMap, totalProjects); } protected virtual bool IsExclusive() 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 e3472e4604f61..979bed9aade96 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.SymbolComputer.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.SymbolComputer.cs @@ -482,7 +482,7 @@ private static ImmutableArray GetReceiverTypeNames(ITypeSymbol receiverT { using var _ = PooledHashSet.GetInstance(out var allTypeNamesBuilder); AddNamesForTypeWorker(receiverTypeSymbol, allTypeNamesBuilder); - return allTypeNamesBuilder.ToImmutableArray(); + return [.. allTypeNamesBuilder]; static void AddNamesForTypeWorker(ITypeSymbol receiverTypeSymbol, PooledHashSet builder) { diff --git a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs index d63813efdca06..13273cb105550 100644 --- a/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs +++ b/src/Features/Core/Portable/Completion/Providers/ImportCompletionProvider/ExtensionMethodImportCompletionHelper.cs @@ -72,7 +72,7 @@ public static void WarmUpCacheInCurrentProcess(Project project) var remoteResult = await client.TryInvokeAsync( project, (service, solutionInfo, cancellationToken) => service.GetUnimportedExtensionMethodsAsync( - solutionInfo, document.Id, position, receiverTypeSymbolKeyData, namespaceInScope.ToImmutableArray(), + solutionInfo, document.Id, position, receiverTypeSymbolKeyData, [.. namespaceInScope], targetTypesSymbolKeyData, forceCacheCreation, hideAdvancedMembers, cancellationToken), cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs b/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs index 869ffdd646947..a4739b0a46790 100644 --- a/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs +++ b/src/Features/Core/Portable/Completion/Providers/RecommendedKeyword.cs @@ -40,6 +40,6 @@ internal static ImmutableArray CreateDisplayParts(string keyw textContentBuilder.AddText(toolTip); } - return textContentBuilder.ToImmutableArray(); + return [.. textContentBuilder]; } } diff --git a/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs b/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs index 1bdb4ab26c64d..7e41ac5d80b2f 100644 --- a/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs +++ b/src/Features/Core/Portable/Debugging/AbstractBreakpointResolver.cs @@ -147,7 +147,7 @@ private async Task> FindMembersAsync( 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()); + return FindMembers(containers, [.. nameParts]); } } catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken)) diff --git a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs index a1521848a186c..6ac5f90bcae97 100644 --- a/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.cs +++ b/src/Features/Core/Portable/Diagnostics/IDiagnosticAnalyzerService.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.Threading; using System.Threading.Tasks; @@ -47,22 +48,6 @@ internal interface IDiagnosticAnalyzerService /// 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. /// @@ -91,11 +76,13 @@ internal interface IDiagnosticAnalyzerService /// 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); + Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, Func>? getDocumentIds, 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. + /// 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. @@ -197,4 +184,12 @@ public static Task> GetDiagnosticsForSpanAsync(th includeCompilerDiagnostics: true, includeSuppressedDiagnostics, priorityProvider, addOperationScope, diagnosticKind, isExplicit, cancellationToken); } + + public static Task> GetDiagnosticsForIdsAsync( + this IDiagnosticAnalyzerService service, Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) + { + return service.GetDiagnosticsForIdsAsync( + solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, getDocumentIds: null, + includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); + } } diff --git a/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs b/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs index 1671cca3ac7fd..f4075ce374447 100644 --- a/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs +++ b/src/Features/Core/Portable/DocumentHighlighting/AbstractDocumentHighlightsService.cs @@ -63,7 +63,9 @@ public async Task> GetDocumentHighlightsAsync private async Task> GetDocumentHighlightsInCurrentProcessAsync( Document document, int position, IImmutableSet documentsToSearch, HighlightingOptions options, CancellationToken cancellationToken) { - var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); + // Document highlights are not impacted by nullable analysis. Get a semantic model with nullability disabled to + // lower the amount of work we need to do here. + var semanticModel = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false); var result = TryGetEmbeddedLanguageHighlights(document, semanticModel, position, options, cancellationToken); if (!result.IsDefaultOrEmpty) return result; diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs index ff5a2ce294130..62ba99219f3af 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSession.cs @@ -433,7 +433,7 @@ private static ImmutableDictionary> GroupToImmutableDiction foreach (var item in items) { - builder.Add(item.Key, item.ToImmutableArray()); + builder.Add(item.Key, [.. item]); } return builder.ToImmutable(); @@ -836,7 +836,7 @@ public ImmutableHashSet GetModulesPreparedForUpdate() { lock (_instance._modulesPreparedForUpdateGuard) { - return _instance._modulesPreparedForUpdate.ToImmutableHashSet(); + return [.. _instance._modulesPreparedForUpdate]; } } diff --git a/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs b/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs index 2b3d5186583d6..2a9b478b6a437 100644 --- a/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs +++ b/src/Features/Core/Portable/EditAndContinue/DebuggingSessionTelemetry.cs @@ -15,7 +15,7 @@ internal sealed class DebuggingSessionTelemetry(Guid solutionSessionId) internal readonly struct Data(DebuggingSessionTelemetry telemetry) { public readonly Guid SolutionSessionId = telemetry._solutionSessionId; - public readonly ImmutableArray EditSessionData = telemetry._editSessionData.ToImmutableArray(); + public readonly ImmutableArray EditSessionData = [.. telemetry._editSessionData]; public readonly int EmptyEditSessionCount = telemetry._emptyEditSessionCount; public readonly int EmptyHotReloadEditSessionCount = telemetry._emptyHotReloadEditSessionCount; } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs index 9e5ec07398c1c..2812e10592465 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueDocumentAnalysesCache.cs @@ -45,7 +45,7 @@ public async ValueTask> GetDocumentAnaly 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(); + return [.. allResults]; } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs index 1d69bf53c5dd6..c62f2c83f7ef4 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueMethodDebugInfoReader.cs @@ -236,7 +236,7 @@ internal static bool TryGetDocumentChecksum(ISymUnmanagedReader5 symReader, stri } algorithmId = symDocument.GetHashAlgorithm(); - checksum = symDocument.GetChecksum().ToImmutableArray(); + checksum = [.. symDocument.GetChecksum()]; return true; } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs index c950c64aafaf5..010063ad16ddb 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditAndContinueService.cs @@ -120,7 +120,7 @@ private ImmutableArray GetActiveDebuggingSessions() { lock (_debuggingSessions) { - return _debuggingSessions.ToImmutableArray(); + return [.. _debuggingSessions]; } } diff --git a/src/Features/Core/Portable/EditAndContinue/EditSession.cs b/src/Features/Core/Portable/EditAndContinue/EditSession.cs index a44f2e9cad3b1..51057d7b7bcef 100644 --- a/src/Features/Core/Portable/EditAndContinue/EditSession.cs +++ b/src/Features/Core/Portable/EditAndContinue/EditSession.cs @@ -726,7 +726,7 @@ internal static void MergePartialEdits( if (edits.Count == mergedEditsBuilder.Count) { mergedEdits = mergedEditsBuilder.ToImmutable(); - addedSymbols = addedSymbolsBuilder.ToImmutableHashSet(); + addedSymbols = [.. addedSymbolsBuilder]; return; } @@ -780,7 +780,7 @@ internal static void MergePartialEdits( } mergedEdits = mergedEditsBuilder.ToImmutable(); - addedSymbols = addedSymbolsBuilder.ToImmutableHashSet(); + addedSymbols = [.. addedSymbolsBuilder]; } public async ValueTask EmitSolutionUpdateAsync(Solution solution, ActiveStatementSpanProvider solutionActiveStatementSpanProvider, UpdateId updateId, CancellationToken cancellationToken) diff --git a/src/Features/Core/Portable/EditAndContinue/TraceLog.cs b/src/Features/Core/Portable/EditAndContinue/TraceLog.cs index 8c0afaf35a6fd..a96471c54ad53 100644 --- a/src/Features/Core/Portable/EditAndContinue/TraceLog.cs +++ b/src/Features/Core/Portable/EditAndContinue/TraceLog.cs @@ -159,7 +159,7 @@ public void Write(DebuggingSessionId sessionId, ImmutableArray bytes, stri try { path = Path.Combine(CreateSessionDirectory(sessionId, directory), fileName); - File.WriteAllBytes(path, bytes.ToArray()); + File.WriteAllBytes(path, [.. bytes]); } catch (Exception e) { diff --git a/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs b/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs index d13938a57c7ad..31a5509f51e6f 100644 --- a/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs +++ b/src/Features/Core/Portable/ExternalAccess/Watch/Api/WatchHotReloadService.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Immutable; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.EditAndContinue; @@ -14,12 +13,10 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Watch.Api; -internal sealed class WatchHotReloadService +internal sealed class WatchHotReloadService(SolutionServices services, Func>> capabilitiesProvider) { - private sealed class DebuggerService(ImmutableArray capabilities) : IManagedHotReloadService + private sealed class DebuggerService(Func>> capabilitiesProvider) : IManagedHotReloadService { - private readonly ImmutableArray _capabilities = capabilities; - public ValueTask> GetActiveStatementsAsync(CancellationToken cancellationToken) => ValueTaskFactory.FromResult(ImmutableArray.Empty); @@ -27,41 +24,39 @@ public ValueTask GetAvailabilityAsync(Guid module, => ValueTaskFactory.FromResult(new ManagedHotReloadAvailability(ManagedHotReloadAvailabilityStatus.Available)); public ValueTask> GetCapabilitiesAsync(CancellationToken cancellationToken) - => ValueTaskFactory.FromResult(_capabilities); + => capabilitiesProvider(); public ValueTask PrepareModuleForUpdateAsync(Guid module, CancellationToken cancellationToken) => ValueTaskFactory.CompletedTask; } - public readonly struct Update + public readonly struct Update( + Guid moduleId, + ImmutableArray ilDelta, + ImmutableArray metadataDelta, + ImmutableArray pdbDelta, + ImmutableArray updatedTypes, + ImmutableArray requiredCapabilities) { - public readonly Guid ModuleId; - public readonly ImmutableArray ILDelta; - public readonly ImmutableArray MetadataDelta; - public readonly ImmutableArray PdbDelta; - public readonly ImmutableArray UpdatedTypes; - public readonly ImmutableArray RequiredCapabilities; - - internal Update(Guid moduleId, ImmutableArray ilDelta, ImmutableArray metadataDelta, ImmutableArray pdbDelta, ImmutableArray updatedTypes, ImmutableArray requiredCapabilities) - { - ModuleId = moduleId; - ILDelta = ilDelta; - MetadataDelta = metadataDelta; - PdbDelta = pdbDelta; - UpdatedTypes = updatedTypes; - RequiredCapabilities = requiredCapabilities; - } + public readonly Guid ModuleId = moduleId; + public readonly ImmutableArray ILDelta = ilDelta; + public readonly ImmutableArray MetadataDelta = metadataDelta; + public readonly ImmutableArray PdbDelta = pdbDelta; + public readonly ImmutableArray UpdatedTypes = updatedTypes; + public readonly ImmutableArray RequiredCapabilities = requiredCapabilities; } private static readonly ActiveStatementSpanProvider s_solutionActiveStatementSpanProvider = (_, _, _) => ValueTaskFactory.FromResult(ImmutableArray.Empty); - private readonly IEditAndContinueService _encService; + private readonly IEditAndContinueService _encService = services.GetRequiredService().Service; + private DebuggingSessionId _sessionId; - private readonly ImmutableArray _capabilities; public WatchHotReloadService(HostWorkspaceServices services, ImmutableArray capabilities) - => (_encService, _capabilities) = (services.GetRequiredService().Service, capabilities); + : this(services.SolutionServices, () => ValueTaskFactory.FromResult(capabilities)) + { + } /// /// Starts the watcher. @@ -72,7 +67,7 @@ public async Task StartSessionAsync(Solution solution, CancellationToken cancell { var newSessionId = await _encService.StartDebuggingSessionAsync( solution, - new DebuggerService(_capabilities), + new DebuggerService(capabilitiesProvider), NullPdbMatchingSourceTextProvider.Instance, captureMatchingDocuments: [], captureAllMatchingDocuments: true, @@ -82,6 +77,17 @@ public async Task StartSessionAsync(Solution solution, CancellationToken cancell _sessionId = newSessionId; } + /// + /// Invoke when capabilities have changed. + /// + public void CapabilitiesChanged() + { + var sessionId = _sessionId; + Contract.ThrowIfFalse(sessionId != default, "Session has not started"); + + _encService.BreakStateOrCapabilitiesChanged(sessionId, inBreakState: null); + } + /// /// Emits updates for all projects that differ between the given snapshot and the one given to the previous successful call or /// the one passed to for the first invocation. @@ -115,6 +121,7 @@ public void EndSession() { Contract.ThrowIfFalse(_sessionId != default, "Session has not started"); _encService.EndDebuggingSession(_sessionId); + _sessionId = default; } internal TestAccessor GetTestAccessor() diff --git a/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs b/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs index af01a2f0785ee..349b799dcf13d 100644 --- a/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs +++ b/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs @@ -214,7 +214,7 @@ private static async Task ExpandAsync(TSelectionResult selecti } private ImmutableArray GetFormattingRules(Document document) - => ImmutableArray.Create(GetCustomFormattingRule(document)).AddRange(Formatter.GetDefaultFormattingRules(document)); + => [GetCustomFormattingRule(document), .. Formatter.GetDefaultFormattingRules(document)]; private OperationStatus CheckVariableTypes( OperationStatus status, diff --git a/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService.DefinitionTrackingContext.cs b/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService.DefinitionTrackingContext.cs index b5c8b72f60d31..30c8390088926 100644 --- a/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService.DefinitionTrackingContext.cs +++ b/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService.DefinitionTrackingContext.cs @@ -59,7 +59,7 @@ public ImmutableArray GetDefinitions() { lock (_gate) { - return _definitions.ToImmutableArray(); + return [.. _definitions]; } } } diff --git a/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService_FindImplementations.cs b/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService_FindImplementations.cs index 18ef1f1dd5451..9e2ff048580f5 100644 --- a/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService_FindImplementations.cs +++ b/src/Features/Core/Portable/FindUsages/AbstractFindUsagesService_FindImplementations.cs @@ -120,7 +120,7 @@ private static async Task> FindSourceImplementationsAsyn } } - return builder.ToImmutableArray(); + return [.. builder]; static bool AddedAllLocations(ISymbol implementation, HashSet<(string filePath, TextSpan span)> seenLocations) { @@ -153,7 +153,7 @@ private static async Task> FindImplementationsWorkerAsyn } } - return result.ToImmutableArray(); + return [.. result]; } private static async Task> FindSourceAndMetadataImplementationsAsync( @@ -189,7 +189,7 @@ private static async Task> FindSourceAndMetadataImplemen implementationsAndOverrides.Add(symbol); } - return implementationsAndOverrides.ToImmutableArray(); + return [.. implementationsAndOverrides]; } else if (symbol is INamedTypeSymbol { TypeKind: TypeKind.Class } namedType) { diff --git a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs index 5ed4e014de204..590f45b35393c 100644 --- a/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/GenerateEqualsAndGetHashCodeFromMembers/GenerateEqualsAndGetHashCodeFromMembersCodeRefactoringProvider.cs @@ -236,7 +236,7 @@ private async Task> CreateActionsAsync( } var codeActions = await Task.WhenAll(tasks).ConfigureAwait(false); - return codeActions.ToImmutableArray(); + return [.. codeActions]; } diff --git a/src/Features/Core/Portable/GenerateMember/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.AbstractInvocationInfo.cs b/src/Features/Core/Portable/GenerateMember/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.AbstractInvocationInfo.cs index 8ffdab80fa802..7cd296978ce7b 100644 --- a/src/Features/Core/Portable/GenerateMember/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.AbstractInvocationInfo.cs +++ b/src/Features/Core/Portable/GenerateMember/GenerateParameterizedMember/AbstractGenerateParameterizedMemberService.AbstractInvocationInfo.cs @@ -62,7 +62,7 @@ private ITypeParameterSymbol MassageTypeParameter( var nonClassTypes = constraints.Where(ts => ts.TypeKind != TypeKind.Class).ToList(); classTypes = MergeClassTypes(classTypes); - constraints = classTypes.Concat(nonClassTypes).ToList(); + constraints = [.. classTypes, .. nonClassTypes]; if (constraints.SequenceEqual(typeParameter.ConstraintTypes)) { return typeParameter; diff --git a/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.Editor.cs b/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.Editor.cs index 48d4dfc643a5f..8adaaeda55199 100644 --- a/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.Editor.cs +++ b/src/Features/Core/Portable/GenerateType/AbstractGenerateTypeService.Editor.cs @@ -313,7 +313,7 @@ private async Task> GetGenerateInNewFileOper ? folders : _state.SimpleName != _state.NameOrMemberAccessExpression ? containers.ToList() - : _semanticDocument.Document.Folders.ToList(); + : [.. _semanticDocument.Document.Folders]; if (newDocument.Project.Language == _semanticDocument.Document.Project.Language) { @@ -524,8 +524,8 @@ private async Task> GetGenerateIntoExistingD // Populate the ContainerList AddFoldersToNamespaceContainers(containerList, folders); - containers = containerList.ToArray(); - includeUsingsOrImports = string.Join(".", containerList.ToArray()); + containers = [.. containerList]; + includeUsingsOrImports = string.Join(".", [.. containerList]); } // Case 4 : If the type is generated into the same VB project or @@ -538,8 +538,8 @@ private async Task> GetGenerateIntoExistingD { // Populate the ContainerList AddFoldersToNamespaceContainers(containerList, folders); - containers = containerList.ToArray(); - includeUsingsOrImports = string.Join(".", containerList.ToArray()); + containers = [.. containerList]; + includeUsingsOrImports = string.Join(".", [.. containerList]); if (!string.IsNullOrWhiteSpace(rootNamespaceOfTheProjectGeneratedInto)) { includeUsingsOrImports = string.IsNullOrEmpty(includeUsingsOrImports) diff --git a/src/Features/Core/Portable/InheritanceMargin/InheritanceMarginItem.cs b/src/Features/Core/Portable/InheritanceMargin/InheritanceMarginItem.cs index e5fe8ea9ea5f1..4e17a45b3c04e 100644 --- a/src/Features/Core/Portable/InheritanceMargin/InheritanceMarginItem.cs +++ b/src/Features/Core/Portable/InheritanceMargin/InheritanceMarginItem.cs @@ -71,5 +71,5 @@ public bool Equals(InheritanceMarginItem other) => targetItems.IsEmpty ? null : new(lineNumber, topLevelDisplayText, displayTexts, glyph, Order(targetItems)); public static ImmutableArray Order(ImmutableArray targetItems) - => targetItems.OrderBy(t => t.DisplayName).ThenByDescending(t => t.LanguageGlyph).ThenBy(t => t.ProjectName ?? "").ToImmutableArray(); + => [.. targetItems.OrderBy(t => t.DisplayName).ThenByDescending(t => t.LanguageGlyph).ThenBy(t => t.ProjectName ?? "")]; } diff --git a/src/Features/Core/Portable/InlineHints/InlineHintHelpers.cs b/src/Features/Core/Portable/InlineHints/InlineHintHelpers.cs index 1848fe2b0847d..3f7c1c32c7e43 100644 --- a/src/Features/Core/Portable/InlineHints/InlineHintHelpers.cs +++ b/src/Features/Core/Portable/InlineHints/InlineHintHelpers.cs @@ -54,7 +54,7 @@ private static async Task> GetDescriptionAsync(Docume } } - return parts.ToImmutableArray(); + return [.. parts]; } return default; diff --git a/src/Features/Core/Portable/InlineMethod/AbstractInlineMethodRefactoringProvider.InlineContext.cs b/src/Features/Core/Portable/InlineMethod/AbstractInlineMethodRefactoringProvider.InlineContext.cs index efadd9d050f2d..0d890aadf1f0b 100644 --- a/src/Features/Core/Portable/InlineMethod/AbstractInlineMethodRefactoringProvider.InlineContext.cs +++ b/src/Features/Core/Portable/InlineMethod/AbstractInlineMethodRefactoringProvider.InlineContext.cs @@ -395,7 +395,7 @@ public static ImmutableHashSet GetAllSymbols( var operation = semanticModel.GetOperation(methodDeclarationSyntax, cancellationToken); visitor.Visit(operation); - return visitor._allSymbols.ToImmutableHashSet(); + return [.. visitor._allSymbols]; } public override void Visit(IOperation? operation) diff --git a/src/Features/Core/Portable/LanguageServices/AnonymousTypeDisplayService/AbstractStructuralTypeDisplayService.cs b/src/Features/Core/Portable/LanguageServices/AnonymousTypeDisplayService/AbstractStructuralTypeDisplayService.cs index f900b9c23ba54..9f6b7df2c5b39 100644 --- a/src/Features/Core/Portable/LanguageServices/AnonymousTypeDisplayService/AbstractStructuralTypeDisplayService.cs +++ b/src/Features/Core/Portable/LanguageServices/AnonymousTypeDisplayService/AbstractStructuralTypeDisplayService.cs @@ -148,7 +148,7 @@ private static ImmutableArray OrderStructuralTypes( { if (symbol is IMethodSymbol method) { - return structuralTypes.OrderBy( + return [.. structuralTypes.OrderBy( (n1, n2) => { var index1 = method.TypeArguments.IndexOf(n1); @@ -157,11 +157,11 @@ private static ImmutableArray OrderStructuralTypes( index2 = index2 < 0 ? int.MaxValue : index2; return index1 - index2; - }).ToImmutableArray(); + })]; } else if (symbol is IPropertySymbol property) { - return structuralTypes.OrderBy( + return [.. structuralTypes.OrderBy( (n1, n2) => { if (n1.Equals(property.ContainingType) && !n2.Equals(property.ContainingType)) @@ -176,7 +176,7 @@ private static ImmutableArray OrderStructuralTypes( { return 0; } - }).ToImmutableArray(); + })]; } return structuralTypes; diff --git a/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceFileService.cs b/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceFileService.cs index fe3febe0beac0..d0abbcdeb54b1 100644 --- a/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceFileService.cs +++ b/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceFileService.cs @@ -30,7 +30,7 @@ internal class MetadataAsSourceFileService([ImportMany] IEnumerable - private readonly ImmutableArray> _providers = ExtensionOrderer.Order(providers).ToImmutableArray(); + private readonly ImmutableArray> _providers = [.. ExtensionOrderer.Order(providers)]; /// /// Workspace created the first time we generate any metadata for any symbol. diff --git a/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceGeneratedFileInfo.cs b/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceGeneratedFileInfo.cs index bd27fae1aa8bf..3101d90c3049b 100644 --- a/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceGeneratedFileInfo.cs +++ b/src/Features/Core/Portable/MetadataAsSource/MetadataAsSourceGeneratedFileInfo.cs @@ -37,7 +37,7 @@ public MetadataAsSourceGeneratedFileInfo(string rootPath, Workspace sourceWorksp ? sourceProject.ParseOptions : sourceProject.Solution.Services.GetLanguageServices(LanguageName).GetRequiredService().GetDefaultParseOptionsWithLatestLanguageVersion(); - this.References = sourceProject.MetadataReferences.ToImmutableArray(); + this.References = [.. sourceProject.MetadataReferences]; this.AssemblyIdentity = topLevelNamedType.ContainingAssembly.Identity; var extension = LanguageName == LanguageNames.CSharp ? ".cs" : ".vb"; diff --git a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj index f06b4057c313c..0d983d125edbc 100644 --- a/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj +++ b/src/Features/Core/Portable/Microsoft.CodeAnalysis.Features.csproj @@ -50,6 +50,7 @@ + diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs index 3a0fd67c501d9..41ba1ca6bbe81 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.CachedDocumentSearch.cs @@ -84,7 +84,7 @@ public async Task SearchCachedDocumentsAsync( var callback = new NavigateToSearchServiceCallback(onItemFound, onProjectCompleted); await client.TryInvokeAsync( (service, callbackId, cancellationToken) => - service.SearchCachedDocumentsAsync(documentKeys, priorityDocumentKeys, searchPattern, kinds.ToImmutableArray(), callbackId, cancellationToken), + service.SearchCachedDocumentsAsync(documentKeys, priorityDocumentKeys, searchPattern, [.. kinds], callbackId, cancellationToken), callback, cancellationToken).ConfigureAwait(false); return; diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs index 5037c87b9bbc0..ee18144bee5f8 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.GeneratedDocumentSearch.cs @@ -43,7 +43,7 @@ await client.TryInvokeAsync( // compilations would not be shared and we'd have to rebuild them. solution, (service, solutionInfo, callbackId, cancellationToken) => - service.SearchGeneratedDocumentsAsync(solutionInfo, projects.SelectAsArray(p => p.Id), searchPattern, kinds.ToImmutableArray(), callbackId, cancellationToken), + service.SearchGeneratedDocumentsAsync(solutionInfo, projects.SelectAsArray(p => p.Id), searchPattern, [.. kinds], callbackId, cancellationToken), callback, cancellationToken).ConfigureAwait(false); return; diff --git a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs index cfc2f549f72ba..d434504af767c 100644 --- a/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs +++ b/src/Features/Core/Portable/NavigateTo/AbstractNavigateToSearchService.NormalSearch.cs @@ -35,7 +35,7 @@ public async Task SearchDocumentAsync( await client.TryInvokeAsync( document.Project, (service, solutionInfo, callbackId, cancellationToken) => - service.SearchDocumentAsync(solutionInfo, document.Id, searchPattern, kinds.ToImmutableArray(), callbackId, cancellationToken), + service.SearchDocumentAsync(solutionInfo, document.Id, searchPattern, [.. kinds], callbackId, cancellationToken), callback, cancellationToken).ConfigureAwait(false); return; @@ -81,7 +81,7 @@ await client.TryInvokeAsync( // on the oop side. solution, (service, solutionInfo, callbackId, cancellationToken) => - service.SearchProjectsAsync(solutionInfo, projects.SelectAsArray(p => p.Id), priorityDocumentIds, searchPattern, kinds.ToImmutableArray(), callbackId, cancellationToken), + service.SearchProjectsAsync(solutionInfo, projects.SelectAsArray(p => p.Id), priorityDocumentIds, searchPattern, [.. kinds], callbackId, cancellationToken), callback, cancellationToken).ConfigureAwait(false); return; diff --git a/src/Features/Core/Portable/NavigateTo/NavigateToSearcher.cs b/src/Features/Core/Portable/NavigateTo/NavigateToSearcher.cs index 424ed5fb706f1..a11f4ff35d6f6 100644 --- a/src/Features/Core/Portable/NavigateTo/NavigateToSearcher.cs +++ b/src/Features/Core/Portable/NavigateTo/NavigateToSearcher.cs @@ -381,7 +381,7 @@ async Task SearchCoreAsync(IGrouping grouping var searchService = grouping.Key; await processProjectAsync( searchService, - grouping.ToImmutableArray(), + [.. grouping], (project, result) => { // If we're seeing a dupe in another project, then filter it out here. The results from diff --git a/src/Features/Core/Portable/PullMemberUp/AbstractPullMemberUpRefactoringProvider.cs b/src/Features/Core/Portable/PullMemberUp/AbstractPullMemberUpRefactoringProvider.cs index 193ffd4030719..b036ced7dcf76 100644 --- a/src/Features/Core/Portable/PullMemberUp/AbstractPullMemberUpRefactoringProvider.cs +++ b/src/Features/Core/Portable/PullMemberUp/AbstractPullMemberUpRefactoringProvider.cs @@ -104,7 +104,7 @@ private static ImmutableArray FindAllValidDestinations( { var allDestinations = selectedMembers.All(m => m.IsKind(SymbolKind.Field)) ? containingType.GetBaseTypes().ToImmutableArray() - : containingType.AllInterfaces.Concat(containingType.GetBaseTypes()).ToImmutableArray(); + : [.. containingType.AllInterfaces, .. containingType.GetBaseTypes()]; return allDestinations.WhereAsArray(destination => MemberAndDestinationValidator.IsDestinationValid(solution, destination, cancellationToken)); } diff --git a/src/Features/Core/Portable/PullMemberUp/MembersPuller.cs b/src/Features/Core/Portable/PullMemberUp/MembersPuller.cs index 0ab3b58f2910a..7091430a3c1b0 100644 --- a/src/Features/Core/Portable/PullMemberUp/MembersPuller.cs +++ b/src/Features/Core/Portable/PullMemberUp/MembersPuller.cs @@ -489,7 +489,7 @@ private static async Task @ref.GetSyntaxAsync(cancellationToken)); var allSyntaxes = await Task.WhenAll(tasks).ConfigureAwait(false); - symbolToDeclarationsBuilder.Add(memberAnalysisResult.Member, allSyntaxes.ToImmutableArray()); + symbolToDeclarationsBuilder.Add(memberAnalysisResult.Member, [.. allSyntaxes]); } return symbolToDeclarationsBuilder.ToImmutableDictionary(); diff --git a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs index d8e20f3fe9c71..73580d5c070a1 100644 --- a/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs +++ b/src/Features/Core/Portable/QuickInfo/CommonSemanticQuickInfoProvider.cs @@ -164,10 +164,10 @@ protected static Task CreateContentAsync( // if generating quick info for an attribute, prefer bind to the class instead of the constructor if (syntaxFactsService.IsAttributeName(token.Parent!)) { - symbols = symbols.OrderBy((s1, s2) => + symbols = [.. symbols.OrderBy((s1, s2) => s1.Kind == s2.Kind ? 0 : s1.Kind == SymbolKind.NamedType ? -1 : - s2.Kind == SymbolKind.NamedType ? 1 : 0).ToImmutableArray(); + s2.Kind == SymbolKind.NamedType ? 1 : 0)]; } return QuickInfoUtilities.CreateQuickInfoItemAsync( diff --git a/src/Features/Core/Portable/QuickInfo/IndentationHelper.cs b/src/Features/Core/Portable/QuickInfo/IndentationHelper.cs index 09fbb8c8c6fbe..9bbcd5aac43f3 100644 --- a/src/Features/Core/Portable/QuickInfo/IndentationHelper.cs +++ b/src/Features/Core/Portable/QuickInfo/IndentationHelper.cs @@ -73,7 +73,7 @@ public static ImmutableArray GetSpansWithAlignedIndentation( } } - return adjustedClassifiedSpans.ToImmutableArray(); + return [.. adjustedClassifiedSpans]; } else { diff --git a/src/Features/Core/Portable/Shared/Extensions/DocumentExtensions.cs b/src/Features/Core/Portable/Shared/Extensions/DocumentExtensions.cs index 4e1f9c472db23..4326eef9430da 100644 --- a/src/Features/Core/Portable/Shared/Extensions/DocumentExtensions.cs +++ b/src/Features/Core/Portable/Shared/Extensions/DocumentExtensions.cs @@ -68,7 +68,7 @@ public static async Task> GetUnionItemsFromDocumentAndLinkedDo totalItems.AddRange(values.NullToEmpty()); } - return totalItems.ToImmutableArray(); + return [.. totalItems]; } public static async Task IsValidContextForDocumentOrLinkedDocumentsAsync( diff --git a/src/Features/Core/Portable/Shared/Extensions/ISymbolExtensions_Sorting.cs b/src/Features/Core/Portable/Shared/Extensions/ISymbolExtensions_Sorting.cs index 8043821cf95c2..c871967799ac1 100644 --- a/src/Features/Core/Portable/Shared/Extensions/ISymbolExtensions_Sorting.cs +++ b/src/Features/Core/Portable/Shared/Extensions/ISymbolExtensions_Sorting.cs @@ -36,8 +36,7 @@ public static ImmutableArray Sort( var symbolToParameterTypeNames = new ConcurrentDictionary(); string[] getParameterTypeNames(TSymbol s) => GetParameterTypeNames(s, semanticModel, position); - return symbols.OrderBy((s1, s2) => Compare(s1, s2, symbolToParameterTypeNames, getParameterTypeNames)) - .ToImmutableArray(); + return [.. symbols.OrderBy((s1, s2) => Compare(s1, s2, symbolToParameterTypeNames, getParameterTypeNames))]; } private static INamedTypeSymbol GetNamedType(ITypeSymbol type) diff --git a/src/Features/Core/Portable/SignatureHelp/AbstractSignatureHelpProvider.cs b/src/Features/Core/Portable/SignatureHelp/AbstractSignatureHelpProvider.cs index a4d8ee47c11db..a2c755e5f1f09 100644 --- a/src/Features/Core/Portable/SignatureHelp/AbstractSignatureHelpProvider.cs +++ b/src/Features/Core/Portable/SignatureHelp/AbstractSignatureHelpProvider.cs @@ -101,7 +101,7 @@ private static (IList items, int? selectedItem) Filter(IList< var filteredList = items.Where(i => Include(i, parameterNames)).ToList(); var isEmpty = filteredList.Count == 0; if (!selectedItem.HasValue || isEmpty) - return (isEmpty ? items.ToList() : filteredList, selectedItem); + return (isEmpty ? [.. items] : filteredList, selectedItem); // adjust the selected item var selection = items[selectedItem.Value]; diff --git a/src/Features/Core/Portable/SolutionCrawler/IDocumentTrackingService.cs b/src/Features/Core/Portable/SolutionCrawler/IDocumentTrackingService.cs deleted file mode 100644 index 1eafefc749af9..0000000000000 --- a/src/Features/Core/Portable/SolutionCrawler/IDocumentTrackingService.cs +++ /dev/null @@ -1,27 +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 Microsoft.CodeAnalysis.Host; - -namespace Microsoft.CodeAnalysis; - -internal interface IDocumentTrackingService : IWorkspaceService -{ - bool SupportsDocumentTracking { get; } - - /// - /// Get the of the active document. May be null if there is no active document - /// or the active document is not in the workspace. - /// - DocumentId? TryGetActiveDocument(); - - /// - /// Get a read only collection of the s of all the visible documents in the workspace. - /// - ImmutableArray GetVisibleDocuments(); - - event EventHandler ActiveDocumentChanged; -} diff --git a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs index 0591664252acf..523087d5364de 100644 --- a/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs +++ b/src/Features/Core/Portable/StackTraceExplorer/StackTraceExplorerService.cs @@ -106,6 +106,6 @@ private static ImmutableArray GetFileMatches(Solution solution, StackF } } - return potentialMatches.ToImmutableArray(); + return [.. potentialMatches]; } } diff --git a/src/Features/Core/Portable/UnusedReferences/UnusedReferencesRemover.cs b/src/Features/Core/Portable/UnusedReferences/UnusedReferencesRemover.cs index a6ef0fb053b04..92722b2a2f169 100644 --- a/src/Features/Core/Portable/UnusedReferences/UnusedReferencesRemover.cs +++ b/src/Features/Core/Portable/UnusedReferences/UnusedReferencesRemover.cs @@ -256,9 +256,7 @@ internal static ImmutableArray GetAllCompilationAssemblies(ReferenceInfo { var transitiveCompilationAssemblies = reference.Dependencies .SelectMany(dependency => GetAllCompilationAssemblies(dependency)); - return reference.CompilationAssemblies - .Concat(transitiveCompilationAssemblies) - .ToImmutableArray(); + return [.. reference.CompilationAssemblies, .. transitiveCompilationAssemblies]; } public static async Task UpdateReferencesAsync( diff --git a/src/Features/Core/Portable/ValueTracking/ValueTrackingProgressCollector.cs b/src/Features/Core/Portable/ValueTracking/ValueTrackingProgressCollector.cs index 0720c59837c96..36cc24bc78513 100644 --- a/src/Features/Core/Portable/ValueTracking/ValueTrackingProgressCollector.cs +++ b/src/Features/Core/Portable/ValueTracking/ValueTrackingProgressCollector.cs @@ -33,7 +33,7 @@ public ImmutableArray GetItems() { lock (_lock) { - return _items.ToImmutableArray(); + return [.. _items]; } } diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs index 404e302782684..b2da22f5e8157 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/LspFileChangeWatcher.cs @@ -42,7 +42,7 @@ public static bool SupportsLanguageServerHost(LanguageServerHost languageServerH public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirectories) { - return new FileChangeContext(watchedDirectories.ToImmutableArray(), this); + return new FileChangeContext([.. watchedDirectories], this); } private class FileChangeContext : IFileChangeContext diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs index 5b21bd69e817a..3f901a8be52b8 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/FileWatching/SimpleFileChangeWatcher.cs @@ -19,7 +19,7 @@ internal sealed class SimpleFileChangeWatcher : IFileChangeWatcher { public IFileChangeContext CreateContext(params WatchedDirectory[] watchedDirectories) { - return new FileChangeContext(watchedDirectories.ToImmutableArray()); + return new FileChangeContext([.. watchedDirectories]); } private class FileChangeContext : IFileChangeContext diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs index b178281cb3bed..f68f4a7e7d536 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs @@ -185,7 +185,7 @@ private async ValueTask LoadOrReloadProjectsAsync(ImmutableSegmentedList project Contract.ThrowIfNull(LanguageServerHost.Instance, "We don't have an LSP channel yet to send this request through."); var languageServerManager = LanguageServerHost.Instance.GetRequiredLspService(); - var unresolvedParams = new UnresolvedDependenciesParams(projectPaths.ToArray()); + var unresolvedParams = new UnresolvedDependenciesParams([.. projectPaths]); await languageServerManager.SendRequestAsync(ProjectNeedsRestoreName, unresolvedParams, cancellationToken); } diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs index 353aeb665de82..b6142d715962b 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/WorkspaceProject.cs @@ -40,7 +40,7 @@ public async Task AddAdditionalFilesAsync(IReadOnlyList addition await using var _ = disposableBatchScope.ConfigureAwait(false); foreach (var additionalFile in additionalFiles) - _project.AddAdditionalFile(additionalFile.FilePath, folders: additionalFile.FolderNames.ToImmutableArray()); + _project.AddAdditionalFile(additionalFile.FilePath, folders: [.. additionalFile.FolderNames]); } public async Task AddAnalyzerConfigFilesAsync(IReadOnlyList analyzerConfigPaths, CancellationToken cancellationToken) @@ -85,7 +85,7 @@ public async Task AddSourceFilesAsync(IReadOnlyList sourceFiles, await using var _ = disposableBatchScope.ConfigureAwait(false); foreach (var sourceFile in sourceFiles) - _project.AddSourceFile(sourceFile.FilePath, folders: sourceFile.FolderNames.ToImmutableArray()); + _project.AddSourceFile(sourceFile.FilePath, folders: [.. sourceFile.FolderNames]); } public void Dispose() @@ -190,7 +190,7 @@ public async Task SetCommandLineArgumentsAsync(IReadOnlyList arguments, var disposableBatchScope = await _project.CreateBatchScopeAsync(cancellationToken).ConfigureAwait(false); await using var _ = disposableBatchScope.ConfigureAwait(false); - _optionsProcessor.SetCommandLine(arguments.ToImmutableArray()); + _optionsProcessor.SetCommandLine([.. arguments]); } public async Task SetDisplayNameAsync(string displayName, CancellationToken cancellationToken) diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs index 9f91e4a0a2525..7f57a870a5cec 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/LanguageServer/Handler/Restore/RestoreHandler.cs @@ -87,7 +87,7 @@ private static ImmutableArray GetRestorePaths(RestoreParams request, Sol { if (request.ProjectFilePaths.Any()) { - return request.ProjectFilePaths.ToImmutableArray(); + return [.. request.ProjectFilePaths]; } // No file paths were specified - this means we should restore all projects in the solution. diff --git a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/TestDiscoverer.DiscoveryHandler.cs b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/TestDiscoverer.DiscoveryHandler.cs index 69df0964d0b8f..a166543886f52 100644 --- a/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/TestDiscoverer.DiscoveryHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/Testing/TestDiscoverer.DiscoveryHandler.cs @@ -52,7 +52,7 @@ public void HandleRawMessage(string rawMessage) public ImmutableArray GetTestCases() { Contract.ThrowIfFalse(_isComplete, "Tried to get test cases before discovery completed"); - return _testCases.ToImmutableArray(); + return [.. _testCases]; } public bool IsAborted() diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Binary/Microsoft.CommonLanguageServerProtocol.Framework.Binary.csproj b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Binary/Microsoft.CommonLanguageServerProtocol.Framework.Binary.csproj deleted file mode 100644 index b82d6a2a340bd..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Binary/Microsoft.CommonLanguageServerProtocol.Framework.Binary.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - - Library - netstandard2.0 - Microsoft.CommonLanguageServerProtocol.Framework - - - true - Microsoft.CommonLanguageServerProtocol.Framework.Binary - - A legacy binary implementation of Microsoft.CommonLanguageServerProtocol.Framework. - - - - BINARY_COMPAT - - - - - - - diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs index eb1381272b95e..4d6774ecd3ab3 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework.Example/ExampleLanguageServer.cs @@ -27,7 +27,7 @@ protected override ILspServices ConstructLspServices() var serviceCollection = new ServiceCollection(); var _ = AddHandlers(serviceCollection) - .AddSingleton(_logger) + .AddSingleton(Logger) .AddSingleton, ExampleRequestContextFactory>() .AddSingleton(s => HandlerProvider) .AddSingleton, CapabilitiesManager>() diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs index 4af41c4bd96bc..7e36101ec4d64 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractHandlerProvider.cs @@ -13,11 +13,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// Manages handler discovery and distribution. /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractHandlerProvider -#else internal abstract class AbstractHandlerProvider -#endif { /// /// Gets the s for all registered methods. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs index f140ff52b78ee..8dddabcf31dbe 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLanguageServer.cs @@ -18,16 +18,10 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractLanguageServer -#else internal abstract class AbstractLanguageServer -#endif { private readonly JsonRpc _jsonRpc; -#pragma warning disable IDE1006 // Naming Styles - Required for API compat, TODO - https://github.com/dotnet/roslyn/issues/72251 - protected readonly ILspLogger _logger; -#pragma warning restore IDE1006 // Naming Styles + protected readonly ILspLogger Logger; protected readonly JsonSerializer _jsonSerializer; @@ -67,7 +61,7 @@ protected AbstractLanguageServer( JsonSerializer jsonSerializer, ILspLogger logger) { - _logger = logger; + Logger = logger; _jsonRpc = jsonRpc; _jsonSerializer = jsonSerializer; @@ -93,35 +87,20 @@ public void Initialize() /// This should only be called once, and then cached. protected abstract ILspServices ConstructLspServices(); - [Obsolete($"Use {nameof(HandlerProvider)} property instead.", error: false)] - protected virtual IHandlerProvider GetHandlerProvider() - { - var lspServices = _lspServices.Value; - var handlerProvider = new HandlerProvider(lspServices); - SetupRequestDispatcher(handlerProvider); - - return handlerProvider; - } - protected virtual AbstractHandlerProvider HandlerProvider { get { -#pragma warning disable CS0618 // Type or member is obsolete - var handlerProvider = GetHandlerProvider(); -#pragma warning restore CS0618 // Type or member is obsolete - if (handlerProvider is AbstractHandlerProvider abstractHandlerProvider) - { - return abstractHandlerProvider; - } - - return new WrappedHandlerProvider(handlerProvider); + var lspServices = _lspServices.Value; + var handlerProvider = new HandlerProvider(lspServices); + SetupRequestDispatcher(handlerProvider); + return handlerProvider; } } public ILspServices GetLspServices() => _lspServices.Value; - protected virtual void SetupRequestDispatcher(IHandlerProvider handlerProvider) + protected virtual void SetupRequestDispatcher(AbstractHandlerProvider handlerProvider) { var entryPointMethodInfo = typeof(DelegatingEntryPoint).GetMethod(nameof(DelegatingEntryPoint.ExecuteRequestAsync))!; // Get unique set of methods from the handler provider for the default language. @@ -187,7 +166,7 @@ public virtual void OnInitialized() protected virtual IRequestExecutionQueue ConstructRequestExecutionQueue() { var handlerProvider = HandlerProvider; - var queue = new RequestExecutionQueue(this, _logger, handlerProvider); + var queue = new RequestExecutionQueue(this, Logger, handlerProvider); queue.Start(); @@ -201,7 +180,7 @@ protected IRequestExecutionQueue GetRequestExecutionQueue() protected virtual string GetLanguageForRequest(string methodName, JToken? parameters) { - _logger.LogInformation($"Using default language handler for {methodName}"); + Logger.LogInformation($"Using default language handler for {methodName}"); return LanguageServerConstants.DefaultLanguageName; } @@ -328,7 +307,7 @@ async Task Shutdown_NoLockAsync(string message) // Immediately yield so that this does not run under the lock. await Task.Yield(); - _logger.LogInformation(message); + Logger.LogInformation(message); // Allow implementations to do any additional cleanup on shutdown. var lifeCycleManager = GetLspServices().GetRequiredService(); @@ -386,7 +365,7 @@ async Task Exit_NoLockAsync() } finally { - _logger.LogInformation("Exiting server"); + Logger.LogInformation("Exiting server"); _serverExitedSource.TrySetResult(null); } } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs index 41c4bf4001219..b849768f60027 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractLspLogger.cs @@ -9,11 +9,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractLspLogger : ILspLogger -#else internal abstract class AbstractLspLogger : ILspLogger -#endif { public abstract void LogDebug(string message, params object[] @params); public abstract void LogStartContext(string message, params object[] @params); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs index adcc85e1c484b..84849c484dc7a 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestContextFactory.cs @@ -21,11 +21,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// /// The type of the RequestContext to be used by the handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractRequestContextFactory -#else internal abstract class AbstractRequestContextFactory -#endif { /// /// Create a object from the given . diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs index 7e5ea5c8aeb5a..27762af48782d 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractRequestScope.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractRequestScope(string name) : IDisposable -#else internal abstract class AbstractRequestScope(string name) : IDisposable -#endif { public string Name { get; } = name; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs index 7df8edce2f545..c5ccdf85ab942 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/AbstractTelemetryService.cs @@ -7,11 +7,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public abstract class AbstractTelemetryService -#else internal abstract class AbstractTelemetryService -#endif { public abstract AbstractRequestScope CreateRequestScope(string lspMethodName); } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs index ac0e0bd0ad03b..93d0b2f64e2b5 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/HandlerProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// -internal class HandlerProvider : AbstractHandlerProvider, IHandlerProvider +internal class HandlerProvider : AbstractHandlerProvider { private readonly ILspServices _lspServices; private ImmutableDictionary>? _requestHandlers; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs index aeb2a3d698f06..564898a683de1 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializeHandler.cs @@ -11,11 +11,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework.Handlers; [LanguageServerEndpoint("initialize", LanguageServerConstants.DefaultLanguageName)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class InitializeHandler -#else internal class InitializeHandler -#endif : IRequestHandler { private readonly IInitializeManager _capabilitiesManager; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs index 46f87a4c52df4..d4a14851e4a70 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Handlers/InitializedHandler.cs @@ -12,11 +12,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework.Handlers; [LanguageServerEndpoint("initialized", LanguageServerConstants.DefaultLanguageName)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class InitializedHandler : INotificationHandler -#else internal class InitializedHandler : INotificationHandler -#endif { private bool HasBeenInitialized = false; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IHandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IHandlerProvider.cs deleted file mode 100644 index 837cd4b051de5..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IHandlerProvider.cs +++ /dev/null @@ -1,25 +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. - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -using System; -using System.Collections.Immutable; - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// Manages handler discovery and distribution. -/// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IHandlerProvider -#else -internal interface IHandlerProvider -#endif -{ - ImmutableArray GetRegisteredMethods(); - - IMethodHandler GetMethodHandler(string method, Type? requestType, Type? responseType); -} diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs index 13e9fc9293a3e..2a73d500c0919 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IInitializeManager.cs @@ -6,12 +6,7 @@ #nullable enable namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IInitializeManager -#else internal interface IInitializeManager -#endif { /// /// Gets a response to be used for "initialize", completing the negoticaitons between client and server. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs index 73b17b89df6ad..b8f704c68d15f 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILifeCycleManager.cs @@ -13,11 +13,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An optional component to run additional logic when LSP shutdown and exit are called, /// for example logging messages, cleaning up custom resources, etc. /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ILifeCycleManager -#else internal interface ILifeCycleManager -#endif { /// /// Called when the server recieves the LSP exit notification. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs index 3bfe075fc3e0e..e7ac139a4ddc2 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspLogger.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ILspLogger -#else internal interface ILspLogger -#endif { void LogStartContext(string message, params object[] @params); void LogEndContext(string message, params object[] @params); diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs index 14ed351fb7e7c..7b094ed23ea64 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ILspServices.cs @@ -10,12 +10,7 @@ using System.Collections.Immutable; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ILspServices : IDisposable -#else internal interface ILspServices : IDisposable -#endif { T GetRequiredService() where T : notnull; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs index d8299e27250a1..6c637662c618d 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IMethodHandler.cs @@ -10,11 +10,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// /// Top level type for LSP request handler. /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IMethodHandler -#else internal interface IMethodHandler -#endif { /// /// Whether or not the solution state on the server is modified as a part of handling this request. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs index 22c8d8f2c38a1..513a78dcc9c70 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/INotificationHandler.cs @@ -14,11 +14,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An interface for handlers of methods which do not return a response and receive no parameters. /// /// The type of the RequestContext to be used by this handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface INotificationHandler : IMethodHandler -#else internal interface INotificationHandler : IMethodHandler -#endif { Task HandleNotificationAsync(TRequestContext requestContext, CancellationToken cancellationToken); } @@ -28,11 +24,7 @@ internal interface INotificationHandler : IMethodHandler /// /// The type of the Request parameter to be received. /// The type of the RequestContext to be used by this handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface INotificationHandler : IMethodHandler -#else internal interface INotificationHandler : IMethodHandler -#endif { Task HandleNotificationAsync(TRequest request, TRequestContext requestContext, CancellationToken cancellationToken); } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs index 832bbf4fdeae2..23f1ea4cd188e 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IQueueItem.cs @@ -15,11 +15,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An item to be queued for execution. /// /// The type of the request context to be passed along to the handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IQueueItem -#else internal interface IQueueItem -#endif { /// /// Executes the work specified by this queue item. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestContextFactory.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestContextFactory.cs deleted file mode 100644 index ee07e97cfdc0a..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestContextFactory.cs +++ /dev/null @@ -1,42 +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; - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// -/// A factory for creating objects from 's. -/// -/// -/// RequestContext's are useful for passing document context, since by default -/// is run on the queue thread (and thus no mutating requests may be executing simultaneously, preventing possible race conditions). -/// It also allows somewhere to pass things like the or which are useful on a wide variety of requests. -/// -/// -/// The type of the RequestContext to be used by the handler. -[Obsolete($"Use {nameof(AbstractRequestContextFactory)} instead.", error: false)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestContextFactory -#else -internal interface IRequestContextFactory -#endif -{ - /// - /// Create a object from the given . - /// Note - throwing in the implementation of this method will cause the server to shutdown. - /// - /// The from which to create a request. - /// The request parameters. - /// - /// The for this request. - /// This method is called on the queue thread to allow context to be retrieved serially, without the possibility of race conditions from Mutating requests. - Task CreateRequestContextAsync(IQueueItem queueItem, TRequestParam requestParam, CancellationToken cancellationToken); -} diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs index 1268202d4328f..463e597c70cf3 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestExecutionQueue.cs @@ -15,11 +15,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// Queues requests to be executed in the proper order. /// /// The type of the RequestContext to be used by the handler. -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestExecutionQueue : IAsyncDisposable -#else internal interface IRequestExecutionQueue : IAsyncDisposable -#endif { /// /// Queue a request. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs index 2bc711aabe3a2..7d58b68427183 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/IRequestHandler.cs @@ -9,12 +9,7 @@ using System.Threading.Tasks; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestHandler : IMethodHandler -#else internal interface IRequestHandler : IMethodHandler -#endif { /// /// Handles an LSP request in the context of the supplied document and/or solution. @@ -26,11 +21,7 @@ internal interface IRequestHandler : IMeth Task HandleRequestAsync(TRequest request, TRequestContext context, CancellationToken cancellationToken); } -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface IRequestHandler : IMethodHandler -#else internal interface IRequestHandler : IMethodHandler -#endif { /// /// Handles an LSP request in the context of the supplied document and/or solution. diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs index eaf30967bc0b9..8c746ab34f188 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/ITextDocumentIdentifierHandler.cs @@ -7,11 +7,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public interface ITextDocumentIdentifierHandler : ITextDocumentIdentifierHandler -#else internal interface ITextDocumentIdentifierHandler : ITextDocumentIdentifierHandler -#endif { /// /// Gets the identifier of the document from the request, if the request provides one. @@ -19,10 +15,6 @@ internal interface ITextDocumentIdentifierHandler /// Default language name for use with and . diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs index 1f8c71ce2d7ab..a7bb1e13ac058 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/LanguageServerEndpointAttribute.cs @@ -14,11 +14,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// An attribute which identifies the method which an implements. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method, AllowMultiple = false)] -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class LanguageServerEndpointAttribute : Attribute -#else internal class LanguageServerEndpointAttribute : Attribute -#endif { /// /// Contains the method that this implements. @@ -45,6 +41,6 @@ public LanguageServerEndpointAttribute(string method) public LanguageServerEndpointAttribute(string method, string language, params string[] additionalLanguages) { Method = method; - Languages = new[] { language }.Concat(additionalLanguages).ToArray(); + Languages = [language, .. additionalLanguages]; } } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems index 3e94a73112030..4f299de12be5e 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/Microsoft.CommonLanguageServerProtocol.Framework.Shared.projitems @@ -18,7 +18,6 @@ - @@ -26,7 +25,6 @@ - @@ -36,7 +34,6 @@ - \ No newline at end of file diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs index 5cf8e93cc2383..31dff9c3b2b81 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/QueueItem.cs @@ -86,30 +86,16 @@ public static (IQueueItem, Task) Create( return (queueItem, queueItem._completionSource.Task); } -#pragma warning disable CS0618 // Type or member is obsolete public async Task CreateRequestContextAsync(IMethodHandler handler, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); _requestTelemetryScope?.RecordExecutionStart(); - var requestContextFactory = (AbstractRequestContextFactory?)LspServices.TryGetService(typeof(AbstractRequestContextFactory)); - if (requestContextFactory is not null) - { - var context = await requestContextFactory.CreateRequestContextAsync(this, handler, _request, cancellationToken).ConfigureAwait(false); - return context; - } - - var obsoleteContextFactory = (IRequestContextFactory?)LspServices.TryGetService(typeof(IRequestContextFactory)); - if (obsoleteContextFactory is not null) - { - var context = await obsoleteContextFactory.CreateRequestContextAsync(this, _request, cancellationToken).ConfigureAwait(false); - return context; - } - - throw new InvalidOperationException($"No {nameof(AbstractRequestContextFactory)} or {nameof(IRequestContextFactory)} was registered with {nameof(ILspServices)}."); + var requestContextFactory = LspServices.GetRequiredService>(); + var context = await requestContextFactory.CreateRequestContextAsync(this, handler, _request, cancellationToken).ConfigureAwait(false); + return context; } -#pragma warning restore CS0618 // Type or member is obsolete /// /// Processes the queued request. Exceptions will be sent to the task completion source diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs index b24ca7e75352d..cd200a1ca0ec7 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestExecutionQueue.cs @@ -50,11 +50,7 @@ namespace Microsoft.CommonLanguageServerProtocol.Framework; /// more messages, and a new queue will need to be created. /// /// -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class RequestExecutionQueue : IRequestExecutionQueue -#else internal class RequestExecutionQueue : IRequestExecutionQueue -#endif { protected readonly ILspLogger _logger; protected readonly AbstractHandlerProvider _handlerProvider; @@ -75,19 +71,6 @@ internal class RequestExecutionQueue : IRequestExecutionQueue _cancelSource.Token; - [Obsolete($"Use constructor with {nameof(AbstractHandlerProvider)} instead.", error: false)] - public RequestExecutionQueue(AbstractLanguageServer languageServer, ILspLogger logger, IHandlerProvider handlerProvider) - { - _languageServer = languageServer; - _logger = logger; - if (handlerProvider is AbstractHandlerProvider abstractHandlerProvider) - { - _handlerProvider = abstractHandlerProvider; - } - - _handlerProvider = new WrappedHandlerProvider(handlerProvider); - } - public RequestExecutionQueue(AbstractLanguageServer languageServer, ILspLogger logger, AbstractHandlerProvider handlerProvider) { _languageServer = languageServer; diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs index e9f7a2be78ed5..c0b0bd4254d43 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestHandlerMetadata.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public record RequestHandlerMetadata(string MethodName, Type? RequestType, Type? ResponseType, string Language) -#else internal record RequestHandlerMetadata(string MethodName, Type? RequestType, Type? ResponseType, string Language) -#endif { internal string HandlerDescription { get; } = $"{MethodName} ({Language})"; } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs index e689d852425f1..9ad2023834c93 100644 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs +++ b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/RequestShutdownEventArgs.cs @@ -8,12 +8,7 @@ using System; namespace Microsoft.CommonLanguageServerProtocol.Framework; - -#if BINARY_COMPAT // TODO - Remove with https://github.com/dotnet/roslyn/issues/72251 -public class RequestShutdownEventArgs : EventArgs -#else internal class RequestShutdownEventArgs : EventArgs -#endif { public string Message { get; } diff --git a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/WrappedHandlerProvider.cs b/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/WrappedHandlerProvider.cs deleted file mode 100644 index 120b54dfb0c21..0000000000000 --- a/src/Features/LanguageServer/Microsoft.CommonLanguageServerProtocol.Framework/WrappedHandlerProvider.cs +++ /dev/null @@ -1,30 +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. - -// This is consumed as 'generated' code in a source package and therefore requires an explicit nullable enable -#nullable enable - -using System; -using System.Collections.Immutable; - -namespace Microsoft.CommonLanguageServerProtocol.Framework; - -/// -/// Wraps an . -/// -internal sealed class WrappedHandlerProvider : AbstractHandlerProvider -{ - private readonly IHandlerProvider _handlerProvider; - - public WrappedHandlerProvider(IHandlerProvider handlerProvider) - { - _handlerProvider = handlerProvider; - } - - public override IMethodHandler GetMethodHandler(string method, Type? requestType, Type? responseType, string language) - => _handlerProvider.GetMethodHandler(method, requestType, responseType); - - public override ImmutableArray GetRegisteredMethods() - => _handlerProvider.GetRegisteredMethods(); -} diff --git a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs index 2b2b974ac5a7c..8eb7e16616167 100644 --- a/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs +++ b/src/Features/LanguageServer/Protocol/DefaultCapabilitiesProvider.cs @@ -98,7 +98,7 @@ public ServerCapabilities GetCapabilities(ClientCapabilities clientCapabilities) Range = true, Legend = new SemanticTokensLegend { - TokenTypes = SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes.ToArray(), + TokenTypes = [.. SemanticTokensSchema.GetSchema(clientCapabilities.HasVisualStudioLspCapability()).AllTokenTypes], TokenModifiers = SemanticTokensSchema.TokenModifiers } }; diff --git a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs index 890759ebb9ceb..d9b8e4078f89f 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/Extensions.cs @@ -10,8 +10,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.FindUsages; -using Microsoft.CodeAnalysis.LanguageServer.Handler; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Collections; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; diff --git a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs index c6a06c30f422c..a06cee02e928e 100644 --- a/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs +++ b/src/Features/LanguageServer/Protocol/Extensions/ProtocolConversions.cs @@ -390,7 +390,7 @@ public static LSP.Range TextSpanToRange(TextSpan textSpan, SourceText text) else { var newText = await newDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(false); - textChanges = newText.GetTextChanges(oldText).ToImmutableArray(); + textChanges = [.. newText.GetTextChanges(oldText)]; } // Map all the text changes' spans for this document. diff --git a/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs b/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs index f30b9ac390f0d..fde0bc07d9891 100644 --- a/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs +++ b/src/Features/LanguageServer/Protocol/Features/CodeFixes/CodeFixService.ProjectCodeFixProvider.cs @@ -15,7 +15,7 @@ private class ProjectCodeFixProvider : AbstractProjectExtensionProvider { protected override ImmutableArray GetLanguages(ExportCodeFixProviderAttribute exportAttribute) - => exportAttribute.Languages.ToImmutableArray(); + => [.. exportAttribute.Languages]; protected override bool TryGetExtensionsFromReference(AnalyzerReference reference, out ImmutableArray extensions) { diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs index aac07fce0e0f2..be32623daf749 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/DiagnosticAnalyzerService.cs @@ -121,12 +121,6 @@ public Task> GetCachedDiagnosticsAsync(Workspace return analyzer.GetCachedDiagnosticsAsync(workspace.CurrentSolution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); } - public Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - { - var analyzer = CreateIncrementalAnalyzer(solution.Workspace); - return analyzer.GetDiagnosticsAsync(solution, projectId, documentId, includeSuppressedDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); - } - public async Task ForceAnalyzeProjectAsync(Project project, CancellationToken cancellationToken) { var analyzer = CreateIncrementalAnalyzer(project.Solution.Workspace); @@ -134,10 +128,10 @@ public async Task ForceAnalyzeProjectAsync(Project project, CancellationToken ca } public Task> GetDiagnosticsForIdsAsync( - Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) + Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) { var analyzer = CreateIncrementalAnalyzer(solution.Workspace); - return analyzer.GetDiagnosticsForIdsAsync(solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); + return analyzer.GetDiagnosticsForIdsAsync(solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, getDocuments, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics, cancellationToken); } public Task> GetProjectDiagnosticsForIdsAsync( diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs index 05b432309c757..68cdf1724c25b 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -24,7 +24,7 @@ internal partial class DiagnosticIncrementalAnalyzer /// Return all diagnostics that belong to given project for the given StateSets (analyzers) either from cache or by calculating them /// private async Task GetProjectAnalysisDataAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, bool forceAnalyzerRun, CancellationToken cancellationToken) + CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, CancellationToken cancellationToken) { using (Logger.LogBlock(FunctionId.Diagnostics_ProjectDiagnostic, GetProjectLogMessage, project, stateSets, cancellationToken)) { @@ -42,64 +42,7 @@ private async Task GetProjectAnalysisDataAsync( return existingData; } - // PERF: Check whether we want to analyze this project or not. - var fullAnalysisEnabled = GlobalOptions.IsFullSolutionAnalysisEnabled(project.Language, out var compilerFullAnalysisEnabled, out var analyzersFullAnalysisEnabled); - if (forceAnalyzerRun) - { - // We are forcing full solution analysis for all diagnostics. - fullAnalysisEnabled = true; - compilerFullAnalysisEnabled = true; - analyzersFullAnalysisEnabled = true; - } - - if (!fullAnalysisEnabled) - { - Logger.Log(FunctionId.Diagnostics_ProjectDiagnostic, p => $"FSA off ({p.FilePath ?? p.Name})", project); - - // If we are producing document diagnostics for some other document in this project, we still want to show - // certain project-level diagnostics that would cause file-level diagnostics to be broken. We will only do this though if - // some file that's open is depending on this project though -- that way we're going to only be analyzing projects - // that have already had compilations produced for. - var shouldProduceOutput = false; - - var projectDependencyGraph = project.Solution.GetProjectDependencyGraph(); - - foreach (var openDocumentId in project.Solution.Workspace.GetOpenDocumentIds()) - { - if (openDocumentId.ProjectId == project.Id || projectDependencyGraph.DoesProjectTransitivelyDependOnProject(openDocumentId.ProjectId, project.Id)) - { - shouldProduceOutput = true; - break; - } - } - - var results = ImmutableDictionary.Empty; - - if (shouldProduceOutput) - { - (results, _) = await UpdateWithDocumentLoadAndGeneratorFailuresAsync( - results, - project, - version, - cancellationToken).ConfigureAwait(false); - } - - return new ProjectAnalysisData(project.Id, VersionStamp.Default, existingData.Result, results); - } - - // Reduce the state sets to analyze based on individual full solution analysis values - // for compiler diagnostics and analyzers. - if (!compilerFullAnalysisEnabled) - { - Debug.Assert(analyzersFullAnalysisEnabled); - stateSets = stateSets.WhereAsArray(s => !s.Analyzer.IsCompilerAnalyzer()); - } - else if (!analyzersFullAnalysisEnabled) - { - stateSets = stateSets.WhereAsArray(s => s.Analyzer.IsCompilerAnalyzer() || s.Analyzer.IsWorkspaceDiagnosticAnalyzer()); - } - - var result = await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideOptions, stateSets, forceAnalyzerRun, existingData.Result, cancellationToken).ConfigureAwait(false); + var result = await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideOptions, stateSets, existingData.Result, cancellationToken).ConfigureAwait(false); // If project is not loaded successfully, get rid of any semantic errors from compiler analyzer. // Note: In the past when project was not loaded successfully we did not run any analyzers on the project. @@ -169,7 +112,7 @@ private static async Task private async Task> ComputeDiagnosticsAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray ideAnalyzers, bool forcedAnalysis, CancellationToken cancellationToken) + CompilationWithAnalyzers? compilationWithAnalyzers, Project project, ImmutableArray ideAnalyzers, CancellationToken cancellationToken) { try { @@ -179,8 +122,8 @@ private async Task 0) { // calculate regular diagnostic analyzers diagnostics - var resultMap = await _diagnosticAnalyzerRunner.AnalyzeProjectAsync(project, compilationWithAnalyzers, - forcedAnalysis, logPerformanceInfo: false, getTelemetryInfo: true, cancellationToken).ConfigureAwait(false); + var resultMap = await _diagnosticAnalyzerRunner.AnalyzeProjectAsync( + project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: true, cancellationToken).ConfigureAwait(false); result = resultMap.AnalysisResult; @@ -198,7 +141,7 @@ private async Task> ComputeDiagnosticsAsync( - CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, bool forcedAnalysis, + CompilationWithAnalyzers? compilationWithAnalyzers, Project project, IdeAnalyzerOptions ideOptions, ImmutableArray stateSets, ImmutableDictionary existing, CancellationToken cancellationToken) { try @@ -225,12 +168,12 @@ await DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync( compilationWithAnalyzers.AnalysisOptions.ReportSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - var result = await ComputeDiagnosticsAsync(compilationWithReducedAnalyzers, project, ideAnalyzers, forcedAnalysis, cancellationToken).ConfigureAwait(false); + var result = await ComputeDiagnosticsAsync(compilationWithReducedAnalyzers, project, ideAnalyzers, cancellationToken).ConfigureAwait(false); return MergeExistingDiagnostics(version, existing, result); } // we couldn't reduce the set. - return await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideAnalyzers, forcedAnalysis, cancellationToken).ConfigureAwait(false); + return await ComputeDiagnosticsAsync(compilationWithAnalyzers, project, ideAnalyzers, cancellationToken).ConfigureAwait(false); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken)) { diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs index 17751bcd9e7d0..88214593ae6d4 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.InProcOrRemoteHostAnalyzerRunner.cs @@ -50,12 +50,11 @@ public Task> AnalyzeProjectAsync( Project project, CompilationWithAnalyzers compilationWithAnalyzers, - bool forceExecuteAllAnalyzers, bool logPerformanceInfo, bool getTelemetryInfo, CancellationToken cancellationToken) => AnalyzeAsync(documentAnalysisScope: null, project, compilationWithAnalyzers, - isExplicit: false, forceExecuteAllAnalyzers, logPerformanceInfo, getTelemetryInfo, cancellationToken); + isExplicit: false, forceExecuteAllAnalyzers: true, logPerformanceInfo, getTelemetryInfo, cancellationToken); private async Task> AnalyzeAsync( DocumentAnalysisScope? documentAnalysisScope, @@ -214,7 +213,7 @@ private static async Task a // order statesets // order will be in this order // BuiltIn Compiler Analyzer (C#/VB) < Regular DiagnosticAnalyzers < Document/ProjectDiagnosticAnalyzers - OrderedStateSets = StateSetMap.Values.OrderBy(PriorityComparison).ToImmutableArray(); + OrderedStateSets = [.. StateSetMap.Values.OrderBy(PriorityComparison)]; } public HostAnalyzerStateSets WithExcludedAnalyzers(ImmutableHashSet excludedAnalyzers) diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs index cb1dc8c68d0b9..f24820a50f801 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_GetDiagnostics.cs @@ -18,14 +18,11 @@ internal partial class DiagnosticIncrementalAnalyzer public Task> GetCachedDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) => new IdeCachedDiagnosticGetter(this, solution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); - public Task> GetDiagnosticsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId, diagnosticIds: null, shouldIncludeAnalyzer: null, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); - - public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); + public Task> GetDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) + => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId, diagnosticIds, shouldIncludeAnalyzer, getDocuments, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics).GetDiagnosticsAsync(cancellationToken); public Task> GetProjectDiagnosticsForIdsAsync(Solution solution, ProjectId? projectId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, bool includeSuppressedDiagnostics, bool includeNonLocalDocumentDiagnostics, CancellationToken cancellationToken) - => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId: null, diagnosticIds, shouldIncludeAnalyzer, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: false, includeNonLocalDocumentDiagnostics).GetProjectDiagnosticsAsync(cancellationToken); + => new IdeLatestDiagnosticGetter(this, solution, projectId, documentId: null, diagnosticIds, shouldIncludeAnalyzer, getDocuments: null, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: false, includeNonLocalDocumentDiagnostics).GetProjectDiagnosticsAsync(cancellationToken); private abstract class DiagnosticGetter { @@ -38,6 +35,8 @@ private abstract class DiagnosticGetter protected readonly bool IncludeLocalDocumentDiagnostics; protected readonly bool IncludeNonLocalDocumentDiagnostics; + private readonly Func> _getDocuments; + private ImmutableArray.Builder? _lazyDataBuilder; public DiagnosticGetter( @@ -45,6 +44,7 @@ public DiagnosticGetter( Solution solution, ProjectId? projectId, DocumentId? documentId, + Func>? getDocuments, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) @@ -53,6 +53,7 @@ public DiagnosticGetter( Solution = solution; DocumentId = documentId; + _getDocuments = getDocuments ?? (static (project, documentId) => documentId != null ? [documentId] : project.DocumentIds); ProjectId = projectId ?? documentId?.ProjectId; IncludeSuppressedDiagnostics = includeSuppressedDiagnostics; @@ -79,7 +80,7 @@ public async Task> GetDiagnosticsAsync(Cancellati return GetDiagnosticData(); } - var documentIds = DocumentId != null ? [DocumentId] : project.DocumentIds; + var documentIds = _getDocuments(project, DocumentId); // return diagnostics specific to one project or document var includeProjectNonLocalResult = DocumentId == null; @@ -132,7 +133,7 @@ private bool ShouldIncludeSuppressedDiagnostic(DiagnosticData diagnostic) private sealed class IdeCachedDiagnosticGetter : DiagnosticGetter { public IdeCachedDiagnosticGetter(DiagnosticIncrementalAnalyzer owner, Solution solution, ProjectId? projectId, DocumentId? documentId, bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) - : base(owner, solution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) + : base(owner, solution, projectId, documentId, getDocuments: null, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) { } @@ -232,8 +233,9 @@ private sealed class IdeLatestDiagnosticGetter : DiagnosticGetter public IdeLatestDiagnosticGetter( DiagnosticIncrementalAnalyzer owner, Solution solution, ProjectId? projectId, DocumentId? documentId, ImmutableHashSet? diagnosticIds, Func? shouldIncludeAnalyzer, - bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) - : base(owner, solution, projectId, documentId, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) + Func>? getDocuments, + bool includeSuppressedDiagnostics, bool includeLocalDocumentDiagnostics, bool includeNonLocalDocumentDiagnostics) + : base(owner, solution, projectId, documentId, getDocuments, includeSuppressedDiagnostics, includeLocalDocumentDiagnostics, includeNonLocalDocumentDiagnostics) { _diagnosticIds = diagnosticIds; _shouldIncludeAnalyzer = shouldIncludeAnalyzer; @@ -269,7 +271,7 @@ protected override async Task AppendDiagnosticsAsync(Project project, IEnumerabl // unlike the suppressed (disabled) analyzer, we will include hidden diagnostic only analyzers here. var compilation = await CreateCompilationWithAnalyzersAsync(project, ideOptions, stateSets, IncludeSuppressedDiagnostics, cancellationToken).ConfigureAwait(false); - var result = await Owner.GetProjectAnalysisDataAsync(compilation, project, ideOptions, stateSets, forceAnalyzerRun: true, cancellationToken).ConfigureAwait(false); + var result = await Owner.GetProjectAnalysisDataAsync(compilation, project, ideOptions, stateSets, cancellationToken).ConfigureAwait(false); foreach (var stateSet in stateSets) { diff --git a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs index af943468881a4..e7054c9de2c94 100644 --- a/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs +++ b/src/Features/LanguageServer/Protocol/Features/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_IncrementalAnalyzer.cs @@ -37,7 +37,7 @@ public async Task> ForceAnalyzeProjectAsync(Proje compilationWithAnalyzers = await DocumentAnalysisExecutor.CreateCompilationWithAnalyzersAsync(project, ideOptions, activeAnalyzers, includeSuppressedDiagnostics: true, cancellationToken).ConfigureAwait(false); - var result = await GetProjectAnalysisDataAsync(compilationWithAnalyzers, project, ideOptions, stateSets, forceAnalyzerRun: true, cancellationToken).ConfigureAwait(false); + var result = await GetProjectAnalysisDataAsync(compilationWithAnalyzers, project, ideOptions, stateSets, cancellationToken).ConfigureAwait(false); using var _ = ArrayBuilder.GetInstance(out var diagnostics); diff --git a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs index 4f6b49fea36b6..deb7877c1b234 100644 --- a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs +++ b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_OpenDocument.cs @@ -21,7 +21,7 @@ private sealed class OpenDocumentSource(Document document) : AbstractDocumentDia public override bool IsLiveSource() => true; - public override async Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + public override async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) { var designTimeDocument = Document; var designTimeSolution = designTimeDocument.Project.Solution; diff --git a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs index d74ca12db6421..b84fee77d2030 100644 --- a/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs +++ b/src/Features/LanguageServer/Protocol/Features/EditAndContinue/EditAndContinueDiagnosticSource_Workspace.cs @@ -22,7 +22,7 @@ private sealed class ProjectSource(Project project, ImmutableArray true; - public override Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + public override Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) => Task.FromResult(diagnostics); } @@ -31,7 +31,7 @@ private sealed class ClosedDocumentSource(TextDocument document, ImmutableArray< public override bool IsLiveSource() => true; - public override Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + public override Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) => Task.FromResult(diagnostics); } diff --git a/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs b/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs index 8aacbe48a42fe..cc7fc24bc3875 100644 --- a/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.cs +++ b/src/Features/LanguageServer/Protocol/Features/Options/SolutionCrawlerOptionsStorage.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 Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Options; diff --git a/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs b/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs index a5c972d5a0a15..cf1b8f407a589 100644 --- a/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs +++ b/src/Features/LanguageServer/Protocol/Features/UnifiedSuggestions/UnifiedSuggestedActionsSource.cs @@ -401,7 +401,7 @@ private static void AddUnifiedSuggestedActionsSet( sets.Add(new UnifiedSuggestedActionSet( originalSolution, category, - group.ToImmutableArray(), + [.. group], title: null, priority, applicableToSpan: groupKey.Item1.DataLocation.UnmappedFileSpan.GetClampedTextSpan(text))); @@ -711,9 +711,7 @@ private static ImmutableArray GetInitiallyOrderedActi private static ImmutableArray OrderActionSets( ImmutableArray actionSets, TextSpan? selectionOpt) { - return actionSets.OrderByDescending(s => s.Priority) - .ThenBy(s => s, new UnifiedSuggestedActionSetComparer(selectionOpt)) - .ToImmutableArray(); + return [.. actionSets.OrderByDescending(s => s.Priority).ThenBy(s => s, new UnifiedSuggestedActionSetComparer(selectionOpt))]; } private static UnifiedSuggestedActionSet WithPriority( diff --git a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs index b8ac33f20ddb6..44136ac1f6479 100644 --- a/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs +++ b/src/Features/LanguageServer/Protocol/Handler/CodeActions/CodeActionHelpers.cs @@ -124,12 +124,12 @@ private static LSP.CodeAction[] GenerateCodeActions( { CommandIdentifier = CodeActionsHandler.RunNestedCodeActionCommandName, Title = title, - Arguments = [new CodeActionResolveData(title, codeAction.CustomTags, request.Range, request.TextDocument, codeActionPathList.ToArray(), fixAllFlavors: null, nestedCodeActions: nestedCodeActions)] + Arguments = [new CodeActionResolveData(title, codeAction.CustomTags, request.Range, request.TextDocument, [.. codeActionPathList], fixAllFlavors: null, nestedCodeActions: nestedCodeActions)] }; } AddLSPCodeActions(builder, codeAction, request, codeActionKind, diagnosticsForFix, nestedCodeActionCommand, - nestedCodeActions, codeActionPathList.ToArray(), suggestedAction); + nestedCodeActions, [.. codeActionPathList], suggestedAction); return builder.ToArray(); } @@ -161,7 +161,7 @@ private static LSP.CodeAction[] GenerateCodeActions( if (!isTopLevelCodeAction) { AddLSPCodeActions(nestedCodeActions, codeAction, request, codeActionKind, diagnosticsForFix, - nestedCodeActionCommand: null, nestedCodeActions: null, pathOfParentAction.ToArray(), suggestedAction); + nestedCodeActionCommand: null, nestedCodeActions: null, [.. pathOfParentAction], suggestedAction); } } @@ -240,7 +240,7 @@ private static VSInternalCodeAction GenerateVSCodeAction( Priority = UnifiedSuggestedActionSetPriorityToPriorityLevel(setPriority), Group = $"Roslyn{currentSetNumber}", ApplicableRange = applicableRange, - Data = new CodeActionResolveData(codeAction.Title, codeAction.CustomTags, request.Range, request.TextDocument, fixAllFlavors: null, nestedCodeActions: null, codeActionPath: codeActionPathList.ToArray()) + Data = new CodeActionResolveData(codeAction.Title, codeAction.CustomTags, request.Range, request.TextDocument, fixAllFlavors: null, nestedCodeActions: null, codeActionPath: [.. codeActionPathList]) }; static VSInternalCodeAction[] GenerateNestedVSCodeActions( diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs index dc20144b584f1..106d2dc6bfc78 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractDocumentPullDiagnosticHandler.cs @@ -2,17 +2,21 @@ // 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Microsoft.CommonLanguageServerProtocol.Framework; using Roslyn.LanguageServer.Protocol; -using LSP = Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal abstract class AbstractDocumentPullDiagnosticHandler( IDiagnosticAnalyzerService diagnosticAnalyzerService, IDiagnosticsRefresher diagnosticRefresher, + IDiagnosticSourceManager diagnosticSourceManager, IGlobalOptionService globalOptions) : AbstractPullDiagnosticHandler( diagnosticAnalyzerService, @@ -20,5 +24,31 @@ internal abstract class AbstractDocumentPullDiagnosticHandler where TDiagnosticsParams : IPartialResultParams { - public abstract LSP.TextDocumentIdentifier? GetTextDocumentIdentifier(TDiagnosticsParams diagnosticsParams); + protected readonly IDiagnosticSourceManager DiagnosticSourceManager = diagnosticSourceManager; + + public abstract TextDocumentIdentifier? GetTextDocumentIdentifier(TDiagnosticsParams diagnosticsParams); + + protected override ValueTask> GetOrderedDiagnosticSourcesAsync(TDiagnosticsParams diagnosticsParams, string? requestDiagnosticCategory, RequestContext context, CancellationToken cancellationToken) + { + // Note: context.Document may be null in the case where the client is asking about a document that we have + // since removed from the workspace. In this case, we don't really have anything to process. + // GetPreviousResults will be used to properly realize this and notify the client that the doc is gone. + // + // Only consider open documents here (and only closed ones in the WorkspacePullDiagnosticHandler). Each + // handler treats those as separate worlds that they are responsible for. + var textDocument = context.TextDocument; + if (textDocument is null) + { + context.TraceInformation("Ignoring diagnostics request because no text document was provided"); + return new([]); + } + + if (!context.IsTracking(textDocument.GetURI())) + { + context.TraceWarning($"Ignoring diagnostics request for untracked document: {textDocument.GetURI()}"); + return new([]); + } + + return DiagnosticSourceManager.CreateDocumentDiagnosticSourcesAsync(context, requestDiagnosticCategory, cancellationToken); + } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs index 755e5d771299d..94bb3f537e1c9 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractPullDiagnosticHandler.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.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; @@ -38,8 +37,6 @@ internal abstract partial class AbstractPullDiagnosticHandler protected const int WorkspaceDiagnosticIdentifier = 1; protected const int DocumentDiagnosticIdentifier = 2; - // internal for testing purposes - internal const int DocumentNonLocalDiagnosticIdentifier = 3; private readonly IDiagnosticsRefresher _diagnosticRefresher; protected readonly IGlobalOptionService GlobalOptions; @@ -68,8 +65,6 @@ protected AbstractPullDiagnosticHandler( GlobalOptions = globalOptions; } - protected virtual string? GetDiagnosticSourceIdentifier(TDiagnosticsParams diagnosticsParams) => null; - /// /// Retrieve the previous results we reported. Used so we can avoid resending data for unchanged files. Also /// used so we can report which documents were removed and can have all their diagnostics cleared. @@ -80,7 +75,7 @@ protected AbstractPullDiagnosticHandler( /// Returns all the documents that should be processed in the desired order to process them in. /// protected abstract ValueTask> GetOrderedDiagnosticSourcesAsync( - TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken); + TDiagnosticsParams diagnosticsParams, string? requestDiagnosticCategory, RequestContext context, CancellationToken cancellationToken); /// /// Creates the appropriate LSP type to report a new set of diagnostics and resultId. @@ -106,7 +101,7 @@ protected abstract ValueTask> GetOrderedDiagno /// protected abstract DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource); - protected abstract string? GetDiagnosticCategory(TDiagnosticsParams diagnosticsParams); + protected abstract string? GetRequestDiagnosticCategory(TDiagnosticsParams diagnosticsParams); /// /// Used by public workspace pull diagnostics to allow it to keep the connection open until @@ -134,9 +129,8 @@ protected virtual Task WaitForChangesAsync(RequestContext context, CancellationT Contract.ThrowIfNull(context.Solution); var clientCapabilities = context.GetRequiredClientCapabilities(); - var category = GetDiagnosticCategory(diagnosticsParams) ?? ""; - var sourceIdentifier = GetDiagnosticSourceIdentifier(diagnosticsParams) ?? ""; - var handlerName = $"{this.GetType().Name}(category: {category}, source: {sourceIdentifier})"; + var category = GetRequestDiagnosticCategory(diagnosticsParams); + var handlerName = $"{this.GetType().Name}(category: {category})"; context.TraceInformation($"{handlerName} started getting diagnostics"); var versionedCache = _categoryToVersionedCache.GetOrAdd(handlerName, static handlerName => new(handlerName)); @@ -160,7 +154,7 @@ protected virtual Task WaitForChangesAsync(RequestContext context, CancellationT // Next process each file in priority order. Determine if diagnostics are changed or unchanged since the // last time we notified the client. Report back either to the client so they can update accordingly. var orderedSources = await GetOrderedDiagnosticSourcesAsync( - diagnosticsParams, context, cancellationToken).ConfigureAwait(false); + diagnosticsParams, category, context, cancellationToken).ConfigureAwait(false); context.TraceInformation($"Processing {orderedSources.Length} documents"); @@ -295,7 +289,7 @@ private async Task ComputeAndReportCurrentDiagnosticsAsync( CancellationToken cancellationToken) { using var _ = ArrayBuilder.GetInstance(out var result); - var diagnostics = await diagnosticSource.GetDiagnosticsAsync(DiagnosticAnalyzerService, context, cancellationToken).ConfigureAwait(false); + var diagnostics = await diagnosticSource.GetDiagnosticsAsync(context, cancellationToken).ConfigureAwait(false); // If we can't get a text document identifier we can't report diagnostics for this source. // This can happen for 'fake' projects (e.g. used for TS script blocks). diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs index f3911586aad23..7e3d6e4619791 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/AbstractWorkspacePullDiagnosticsHandler.cs @@ -3,21 +3,13 @@ // 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 System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; -using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Collections; -using Microsoft.CodeAnalysis.SolutionCrawler; -using Microsoft.CodeAnalysis.TaskList; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -27,6 +19,7 @@ internal abstract class AbstractWorkspacePullDiagnosticsHandler /// Flag that represents whether the LSP view of the world has changed. @@ -40,9 +33,11 @@ protected AbstractWorkspacePullDiagnosticsHandler( LspWorkspaceManager workspaceManager, LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService diagnosticAnalyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) : base(diagnosticAnalyzerService, diagnosticRefresher, globalOptions) { + DiagnosticSourceManager = diagnosticSourceManager; _workspaceManager = workspaceManager; _workspaceRegistrationService = registrationService; @@ -56,32 +51,17 @@ public void Dispose() _workspaceRegistrationService.LspSolutionChanged -= OnLspSolutionChanged; } - protected override async ValueTask> GetOrderedDiagnosticSourcesAsync(TDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken) + protected override ValueTask> GetOrderedDiagnosticSourcesAsync(TDiagnosticsParams diagnosticsParams, string? requestDiagnosticCategory, RequestContext context, CancellationToken cancellationToken) { - // If we're being called from razor, we do not support WorkspaceDiagnostics at all. For razor, workspace - // diagnostics will be handled by razor itself, which will operate by calling into Roslyn and asking for - // document-diagnostics instead. if (context.ServerKind == WellKnownLspServerKinds.RazorLspServer) - return []; - - Contract.ThrowIfNull(context.Solution); - - var category = GetDiagnosticCategory(diagnosticsParams); - - // TODO: Implement as extensibility point. https://github.com/dotnet/roslyn/issues/72896 - - if (category == PullDiagnosticCategories.Task) - return GetTaskListDiagnosticSources(context, GlobalOptions); - - if (category == PullDiagnosticCategories.EditAndContinue) - return await EditAndContinueDiagnosticSource.CreateWorkspaceDiagnosticSourcesAsync(context.Solution, document => context.IsTracking(document.GetURI()), cancellationToken).ConfigureAwait(false); - - // if this request doesn't have a category at all (legacy behavior, assume they're asking about everything). - if (category == null || category == PullDiagnosticCategories.WorkspaceDocumentsAndProject) - return await GetDiagnosticSourcesAsync(context, GlobalOptions, cancellationToken).ConfigureAwait(false); + { + // If we're being called from razor, we do not support WorkspaceDiagnostics at all. For razor, workspace + // diagnostics will be handled by razor itself, which will operate by calling into Roslyn and asking for + // document-diagnostics instead. + return new([]); + } - // if it's a category we don't recognize, return nothing. - return []; + return DiagnosticSourceManager.CreateWorkspaceDiagnosticSourcesAsync(context, requestDiagnosticCategory, cancellationToken); } private void OnLspSolutionChanged(object? sender, WorkspaceChangeEventArgs e) @@ -115,159 +95,6 @@ protected override async Task WaitForChangesAsync(RequestContext context, Cancel return; } - private static ImmutableArray GetTaskListDiagnosticSources( - RequestContext context, IGlobalOptionService globalOptions) - { - Contract.ThrowIfNull(context.Solution); - - // Only compute task list items for closed files if the option is on for it. - var taskListEnabled = globalOptions.GetTaskListOptions().ComputeForClosedFiles; - if (!taskListEnabled) - return []; - - using var _ = ArrayBuilder.GetInstance(out var result); - - foreach (var project in GetProjectsInPriorityOrder(context.Solution, context.SupportedLanguages)) - { - foreach (var document in project.Documents) - { - if (!ShouldSkipDocument(context, document)) - result.Add(new TaskListDiagnosticSource(document, globalOptions)); - } - } - - return result.ToImmutableAndClear(); - } - - /// - /// There are three potential sources for reporting workspace diagnostics: - /// - /// 1. Full solution analysis: If the user has enabled Full solution analysis, we always run analysis on the latest - /// project snapshot and return up-to-date diagnostics computed from this analysis. - /// - /// 2. Code analysis service: Otherwise, if full solution analysis is disabled, and if we have diagnostics from an explicitly - /// triggered code analysis execution on either the current or a prior project snapshot, we return - /// diagnostics from this execution. These diagnostics may be stale with respect to the current - /// project snapshot, but they match user's intent of not enabling continuous background analysis - /// for always having up-to-date workspace diagnostics, but instead computing them explicitly on - /// specific project snapshots by manually running the "Run Code Analysis" command on a project or solution. - /// - /// 3. EnC analysis: Emit and debugger diagnostics associated with a closed document or not associated with any document. - /// - /// If full solution analysis is disabled AND code analysis was never executed for the given project, - /// we have no workspace diagnostics to report and bail out. - /// - public static async ValueTask> GetDiagnosticSourcesAsync( - RequestContext context, IGlobalOptionService globalOptions, CancellationToken cancellationToken) - { - Contract.ThrowIfNull(context.Solution); - - using var _ = ArrayBuilder.GetInstance(out var result); - - var solution = context.Solution; - var enableDiagnosticsInSourceGeneratedFiles = solution.Services.GetService()?.EnableDiagnosticsInSourceGeneratedFiles == true; - var codeAnalysisService = solution.Services.GetRequiredService(); - - foreach (var project in GetProjectsInPriorityOrder(solution, context.SupportedLanguages)) - await AddDocumentsAndProjectAsync(project, cancellationToken).ConfigureAwait(false); - - return result.ToImmutableAndClear(); - - async Task AddDocumentsAndProjectAsync(Project project, CancellationToken cancellationToken) - { - var fullSolutionAnalysisEnabled = globalOptions.IsFullSolutionAnalysisEnabled(project.Language, out var compilerFullSolutionAnalysisEnabled, out var analyzersFullSolutionAnalysisEnabled); - if (!fullSolutionAnalysisEnabled && !codeAnalysisService.HasProjectBeenAnalyzed(project.Id)) - return; - - Func? shouldIncludeAnalyzer = !compilerFullSolutionAnalysisEnabled || !analyzersFullSolutionAnalysisEnabled - ? ShouldIncludeAnalyzer : null; - - AddDocumentSources(project.Documents); - AddDocumentSources(project.AdditionalDocuments); - - // If all features are enabled for source generated documents, then compute todo-comments/diagnostics for them. - if (enableDiagnosticsInSourceGeneratedFiles) - { - var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); - AddDocumentSources(sourceGeneratedDocuments); - } - - // Finally, add the appropriate FSA or CodeAnalysis project source to get project specific diagnostics, not associated with any document. - AddProjectSource(); - - return; - - void AddDocumentSources(IEnumerable documents) - { - foreach (var document in documents) - { - if (!ShouldSkipDocument(context, document)) - { - // Add the appropriate FSA or CodeAnalysis document source to get document diagnostics. - var documentDiagnosticSource = fullSolutionAnalysisEnabled - ? AbstractWorkspaceDocumentDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(document, shouldIncludeAnalyzer) - : AbstractWorkspaceDocumentDiagnosticSource.CreateForCodeAnalysisDiagnostics(document, codeAnalysisService); - result.Add(documentDiagnosticSource); - } - } - } - - void AddProjectSource() - { - var projectDiagnosticSource = fullSolutionAnalysisEnabled - ? AbstractProjectDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(project, shouldIncludeAnalyzer) - : AbstractProjectDiagnosticSource.CreateForCodeAnalysisDiagnostics(project, codeAnalysisService); - result.Add(projectDiagnosticSource); - } - - bool ShouldIncludeAnalyzer(DiagnosticAnalyzer analyzer) - => analyzer.IsCompilerAnalyzer() ? compilerFullSolutionAnalysisEnabled : analyzersFullSolutionAnalysisEnabled; - } - } - - private static IEnumerable GetProjectsInPriorityOrder( - Solution solution, ImmutableArray supportedLanguages) - { - return GetProjectsInPriorityOrderWorker(solution) - .WhereNotNull() - .Distinct() - .Where(p => supportedLanguages.Contains(p.Language)); - - static IEnumerable GetProjectsInPriorityOrderWorker(Solution solution) - { - var documentTrackingService = solution.Services.GetRequiredService(); - - // Collect all the documents from the solution in the order we'd like to get diagnostics for. This will - // prioritize the files from currently active projects, but then also include all other docs in all projects - // (depending on current FSA settings). - - var activeDocument = documentTrackingService.GetActiveDocument(solution); - var visibleDocuments = documentTrackingService.GetVisibleDocuments(solution); - - yield return activeDocument?.Project; - foreach (var doc in visibleDocuments) - yield return doc.Project; - - foreach (var project in solution.Projects) - yield return project; - } - } - - private static bool ShouldSkipDocument(RequestContext context, TextDocument document) - { - // Only consider closed documents here (and only open ones in the DocumentPullDiagnosticHandler). - // Each handler treats those as separate worlds that they are responsible for. - if (context.IsTracking(document.GetURI())) - { - context.TraceInformation($"Skipping tracked document: {document.GetURI()}"); - return true; - } - - // Do not attempt to get workspace diagnostics for Razor files, Razor will directly ask us for document diagnostics - // for any razor file they are interested in. - return document.IsRazorDocument(); - } - internal abstract TestAccessor GetTestAccessor(); internal readonly struct TestAccessor(AbstractWorkspacePullDiagnosticsHandler handler) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DiagnosticSourceManager.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DiagnosticSourceManager.cs new file mode 100644 index 0000000000000..1b870ac2dc45f --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DiagnosticSourceManager.cs @@ -0,0 +1,160 @@ +// 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.Composition; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; +using Microsoft.CodeAnalysis.PooledObjects; +using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceManager)), Shared] +internal sealed class DiagnosticSourceManager : IDiagnosticSourceManager +{ + /// + /// Document level providers ordered by name. + /// + private readonly ImmutableDictionary _nameToDocumentProviderMap; + + /// + /// Workspace level providers ordered by name. + /// + private readonly ImmutableDictionary _nameToWorkspaceProviderMap; + + [ImportingConstructor] + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + public DiagnosticSourceManager([ImportMany] IEnumerable sourceProviders) + { + _nameToDocumentProviderMap = sourceProviders + .Where(p => p.IsDocument) + .ToImmutableDictionary(kvp => kvp.Name, kvp => kvp); + + _nameToWorkspaceProviderMap = sourceProviders + .Where(p => !p.IsDocument) + .ToImmutableDictionary(kvp => kvp.Name, kvp => kvp); + } + + public ImmutableArray GetDocumentSourceProviderNames() + => _nameToDocumentProviderMap.Keys.ToImmutableArray(); + + public ImmutableArray GetWorkspaceSourceProviderNames() + => _nameToWorkspaceProviderMap.Keys.ToImmutableArray(); + + public ValueTask> CreateDocumentDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken) + => CreateDiagnosticSourcesAsync(context, providerName, _nameToDocumentProviderMap, isDocument: true, cancellationToken); + + public ValueTask> CreateWorkspaceDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken) + => CreateDiagnosticSourcesAsync(context, providerName, _nameToWorkspaceProviderMap, isDocument: false, cancellationToken); + + private static async ValueTask> CreateDiagnosticSourcesAsync( + RequestContext context, + string? providerName, + ImmutableDictionary nameToProviderMap, + bool isDocument, + CancellationToken cancellationToken) + { + if (providerName != null) + { + // VS does not distinguish between document and workspace sources. Thus it can request + // document diagnostics with workspace source name. We need to handle this case. + if (nameToProviderMap.TryGetValue(providerName, out var provider)) + return await provider.CreateDiagnosticSourcesAsync(context, cancellationToken).ConfigureAwait(false); + + return []; + } + else + { + // VS Code (and legacy VS ?) pass null sourceName when requesting all sources. + using var _ = ArrayBuilder.GetInstance(out var sourcesBuilder); + foreach (var (name, provider) in nameToProviderMap) + { + // Exclude Task diagnostics from the aggregated sources. + if (name != PullDiagnosticCategories.Task) + { + var namedSources = await provider.CreateDiagnosticSourcesAsync(context, cancellationToken).ConfigureAwait(false); + sourcesBuilder.AddRange(namedSources); + } + } + + var sources = sourcesBuilder.ToImmutableAndClear(); + return AggregateSourcesIfNeeded(sources, isDocument); + } + } + + public static ImmutableArray AggregateSourcesIfNeeded(ImmutableArray sources, bool isDocument) + { + if (sources.Length <= 1) + { + return sources; + } + + if (isDocument) + { + // Group all document sources into a single source. + Debug.Assert(sources.All(s => s.IsLiveSource()), "All document sources should be live"); + sources = [new AggregatedDocumentDiagnosticSource(sources)]; + } + else + { + // We ASSUME that all sources with the same ProjectOrDocumentID and IsLiveSource + // will have same value for GetDocumentIdentifier and GetProject(). Thus can be + // aggregated in a single source which will return same values. See + // AggregatedDocumentDiagnosticSource implementation for more details. + sources = sources.GroupBy(s => (s.GetId(), s.IsLiveSource()), s => s) + .SelectMany(g => AggregatedDocumentDiagnosticSource.AggregateIfNeeded(g)) + .ToImmutableArray(); + } + + return sources; + } + + /// + /// Aggregates multiple s into a single source. + /// + /// Sources to aggregate + /// + /// Aggregation is usually needed for clients like VS Code which supports single source per request. + /// + private sealed class AggregatedDocumentDiagnosticSource(ImmutableArray sources) : IDiagnosticSource + { + public static ImmutableArray AggregateIfNeeded(IEnumerable sources) + { + var result = sources.ToImmutableArray(); + if (result.Length > 1) + { + result = [new AggregatedDocumentDiagnosticSource(result)]; + } + + return result; + } + + public bool IsLiveSource() => true; + public Project GetProject() => sources[0].GetProject(); + public ProjectOrDocumentId GetId() => sources[0].GetId(); + public TextDocumentIdentifier? GetDocumentIdentifier() => sources[0].GetDocumentIdentifier(); + public string ToDisplayString() => $"{this.GetType().Name}: count={sources.Length}"; + + public async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) + { + using var _ = ArrayBuilder.GetInstance(out var diagnostics); + foreach (var source in sources) + { + var namedDiagnostics = await source.GetDiagnosticsAsync(context, cancellationToken).ConfigureAwait(false); + diagnostics.AddRange(namedDiagnostics); + } + + return diagnostics.ToImmutableAndClear(); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DocumentSyntaxAndSemanticDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DocumentSyntaxAndSemanticDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..2f0f50ec3f604 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/DocumentSyntaxAndSemanticDiagnosticSourceProvider.cs @@ -0,0 +1,67 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +internal abstract class AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider( + IDiagnosticAnalyzerService diagnosticAnalyzerService, DiagnosticKind kind, string sourceName) + : IDiagnosticSourceProvider +{ + public bool IsDocument => true; + public string Name => sourceName; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.GetTrackedDocument() is { } document) + { + return new([new DocumentDiagnosticSource(diagnosticAnalyzerService, kind, document)]); + } + + return new([]); + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentCompilerSyntaxDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider(diagnosticAnalyzerService, + DiagnosticKind.CompilerSyntax, PullDiagnosticCategories.DocumentCompilerSyntax) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentCompilerSemanticDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider( + diagnosticAnalyzerService, DiagnosticKind.CompilerSemantic, PullDiagnosticCategories.DocumentCompilerSemantic) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentAnalyzerSyntaxDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider(diagnosticAnalyzerService, + DiagnosticKind.AnalyzerSyntax, PullDiagnosticCategories.DocumentAnalyzerSyntax) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentAnalyzerSemanticDiagnosticSourceProvider([Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : AbstractDocumentSyntaxAndSemanticDiagnosticSourceProvider(diagnosticAnalyzerService, + DiagnosticKind.AnalyzerSemantic, PullDiagnosticCategories.DocumentAnalyzerSemantic) + { + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceManager.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceManager.cs new file mode 100644 index 0000000000000..51e0ef8ff0e10 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceManager.cs @@ -0,0 +1,42 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; + +/// +/// Provides centralized/singleton management of MEF based s. +/// Consumers - like diagnostic handlers - use it to get diagnostics from one or more providers. +/// +internal interface IDiagnosticSourceManager +{ + /// + /// Returns the names of document level s. + /// + ImmutableArray GetDocumentSourceProviderNames(); + + /// + /// Returns the names of workspace level s. + /// + ImmutableArray GetWorkspaceSourceProviderNames(); + + /// + /// Creates document diagnostic sources for the given . + /// + /// + /// Optional provider name. If then diagnostics from all providers are used. + /// A cancellation token that can be used to cancel the request processing. + ValueTask> CreateDocumentDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken); + + /// + /// Creates workspace diagnostic sources for the given . + /// + /// + /// Optional provider name. If not specified then diagnostics from all providers are used. + /// A cancellation token that can be used to cancel the request processing. + ValueTask> CreateWorkspaceDiagnosticSourcesAsync(RequestContext context, string? providerName, CancellationToken cancellationToken); +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..daeb1b4d71aee --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/IDiagnosticSourceProvider.cs @@ -0,0 +1,33 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +/// +/// Provides diagnostic sources. +/// +internal interface IDiagnosticSourceProvider +{ + /// + /// if this provider is for documents, if it is for workspace. + /// + bool IsDocument { get; } + + /// + /// Provider's name. Each should have a unique name within scope. + /// + string Name { get; } + + /// + /// Creates the diagnostic sources. + /// + /// + /// A cancellation token that can be used to cancel the request processing. + ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken); +} + diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDiagnosticSourceHelpers.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDiagnosticSourceHelpers.cs new file mode 100644 index 0000000000000..8956840dbbfa2 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDiagnosticSourceHelpers.cs @@ -0,0 +1,56 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis.Host; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +internal static class WorkspaceDiagnosticSourceHelpers +{ + public static IEnumerable GetProjectsInPriorityOrder(Solution solution, ImmutableArray supportedLanguages) + { + return GetProjectsInPriorityOrderWorker(solution) + .WhereNotNull() + .Distinct() + .Where(p => supportedLanguages.Contains(p.Language)); + + static IEnumerable GetProjectsInPriorityOrderWorker(Solution solution) + { + var documentTrackingService = solution.Services.GetRequiredService(); + + // Collect all the documents from the solution in the order we'd like to get diagnostics for. This will + // prioritize the files from currently active projects, but then also include all other docs in all projects + // (depending on current FSA settings). + + var activeDocument = documentTrackingService.GetActiveDocument(solution); + var visibleDocuments = documentTrackingService.GetVisibleDocuments(solution); + + yield return activeDocument?.Project; + foreach (var doc in visibleDocuments) + yield return doc.Project; + + foreach (var project in solution.Projects) + yield return project; + } + } + + public static bool ShouldSkipDocument(RequestContext context, TextDocument document) + { + // Only consider closed documents here (and only open ones in the DocumentPullDiagnosticHandler). + // Each handler treats those as separate worlds that they are responsible for. + if (context.IsTracking(document.GetURI())) + { + context.TraceInformation($"Skipping tracked document: {document.GetURI()}"); + return true; + } + + // Do not attempt to get workspace diagnostics for Razor files, Razor will directly ask us for document diagnostics + // for any razor file they are interested in. + return document.IsRazorDocument(); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDocumentsAndProjectDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDocumentsAndProjectDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..9daa0cf91dd5c --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSourceProviders/WorkspaceDocumentsAndProjectDiagnosticSourceProvider.cs @@ -0,0 +1,115 @@ +// 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.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.SolutionCrawler; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceDocumentsAndProjectDiagnosticSourceProvider( + [Import] IDiagnosticAnalyzerService diagnosticAnalyzerService, + [Import] IGlobalOptionService globalOptions) + : IDiagnosticSourceProvider +{ + public bool IsDocument => false; + public string Name => PullDiagnosticCategories.WorkspaceDocumentsAndProject; + + /// + /// There are three potential sources for reporting workspace diagnostics: + /// + /// 1. Full solution analysis: If the user has enabled Full solution analysis, we always run analysis on the latest + /// project snapshot and return up-to-date diagnostics computed from this analysis. + /// + /// 2. Code analysis service: Otherwise, if full solution analysis is disabled, and if we have diagnostics from an explicitly + /// triggered code analysis execution on either the current or a prior project snapshot, we return + /// diagnostics from this execution. These diagnostics may be stale with respect to the current + /// project snapshot, but they match user's intent of not enabling continuous background analysis + /// for always having up-to-date workspace diagnostics, but instead computing them explicitly on + /// specific project snapshots by manually running the "Run Code Analysis" command on a project or solution. + /// + /// 3. EnC analysis: Emit and debugger diagnostics associated with a closed document or not associated with any document. + /// + /// If full solution analysis is disabled AND code analysis was never executed for the given project, + /// we have no workspace diagnostics to report and bail out. + /// + public async ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + using var _ = ArrayBuilder.GetInstance(out var result); + + var solution = context.Solution; + var enableDiagnosticsInSourceGeneratedFiles = solution.Services.GetService()?.EnableDiagnosticsInSourceGeneratedFiles == true; + var codeAnalysisService = solution.Services.GetRequiredService(); + + foreach (var project in WorkspaceDiagnosticSourceHelpers.GetProjectsInPriorityOrder(solution, context.SupportedLanguages)) + await AddDocumentsAndProjectAsync(project, diagnosticAnalyzerService, cancellationToken).ConfigureAwait(false); + + return result.ToImmutableAndClear(); + + async Task AddDocumentsAndProjectAsync(Project project, IDiagnosticAnalyzerService diagnosticAnalyzerService, CancellationToken cancellationToken) + { + var fullSolutionAnalysisEnabled = globalOptions.IsFullSolutionAnalysisEnabled(project.Language, out var compilerFullSolutionAnalysisEnabled, out var analyzersFullSolutionAnalysisEnabled); + if (!fullSolutionAnalysisEnabled && !codeAnalysisService.HasProjectBeenAnalyzed(project.Id)) + return; + + Func? shouldIncludeAnalyzer = !compilerFullSolutionAnalysisEnabled || !analyzersFullSolutionAnalysisEnabled + ? ShouldIncludeAnalyzer : null; + + AddDocumentSources(project.Documents); + AddDocumentSources(project.AdditionalDocuments); + + // If all features are enabled for source generated documents, then compute todo-comments/diagnostics for them. + if (enableDiagnosticsInSourceGeneratedFiles) + { + var sourceGeneratedDocuments = await project.GetSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false); + AddDocumentSources(sourceGeneratedDocuments); + } + + // Finally, add the appropriate FSA or CodeAnalysis project source to get project specific diagnostics, not associated with any document. + AddProjectSource(); + + return; + + void AddDocumentSources(IEnumerable documents) + { + foreach (var document in documents) + { + if (!WorkspaceDiagnosticSourceHelpers.ShouldSkipDocument(context, document)) + { + // Add the appropriate FSA or CodeAnalysis document source to get document diagnostics. + var documentDiagnosticSource = fullSolutionAnalysisEnabled + ? AbstractWorkspaceDocumentDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(document, diagnosticAnalyzerService, shouldIncludeAnalyzer) + : AbstractWorkspaceDocumentDiagnosticSource.CreateForCodeAnalysisDiagnostics(document, codeAnalysisService); + result.Add(documentDiagnosticSource); + } + } + } + + void AddProjectSource() + { + var projectDiagnosticSource = fullSolutionAnalysisEnabled + ? AbstractProjectDiagnosticSource.CreateForFullSolutionAnalysisDiagnostics(project, diagnosticAnalyzerService, shouldIncludeAnalyzer) + : AbstractProjectDiagnosticSource.CreateForCodeAnalysisDiagnostics(project, codeAnalysisService); + result.Add(projectDiagnosticSource); + } + + bool ShouldIncludeAnalyzer(DiagnosticAnalyzer analyzer) + => analyzer.IsCompilerAnalyzer() ? compilerFullSolutionAnalysisEnabled : analyzersFullSolutionAnalysisEnabled; + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs index 0de186f4e9ac1..f88810864a7bb 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractDocumentDiagnosticSource.cs @@ -18,7 +18,7 @@ internal abstract class AbstractDocumentDiagnosticSource(TDocument do public abstract bool IsLiveSource(); public abstract Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken); + RequestContext context, CancellationToken cancellationToken); public ProjectOrDocumentId GetId() => new(Document.Id); public Project GetProject() => Document.Project; diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs index d34ef1d906826..ac49b0a9da7c6 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractProjectDiagnosticSource.cs @@ -16,14 +16,14 @@ internal abstract class AbstractProjectDiagnosticSource(Project project) { protected Project Project => project; - public static AbstractProjectDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(Project project, Func? shouldIncludeAnalyzer) - => new FullSolutionAnalysisDiagnosticSource(project, shouldIncludeAnalyzer); + public static AbstractProjectDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(Project project, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) + => new FullSolutionAnalysisDiagnosticSource(project, diagnosticAnalyzerService, shouldIncludeAnalyzer); public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) => new CodeAnalysisDiagnosticSource(project, codeAnalysisService); public abstract bool IsLiveSource(); - public abstract Task> GetDiagnosticsAsync(IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken); + public abstract Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken); public ProjectOrDocumentId GetId() => new(Project.Id); public Project GetProject() => Project; @@ -33,7 +33,8 @@ public static AbstractProjectDiagnosticSource CreateForCodeAnalysisDiagnostics(P : null; public string ToDisplayString() => Project.Name; - private sealed class FullSolutionAnalysisDiagnosticSource(Project project, Func? shouldIncludeAnalyzer) : AbstractProjectDiagnosticSource(project) + private sealed class FullSolutionAnalysisDiagnosticSource(Project project, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) + : AbstractProjectDiagnosticSource(project) { /// /// This is a normal project source that represents live/fresh diagnostics that should supersede everything else. @@ -42,7 +43,6 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { @@ -55,7 +55,8 @@ public override async Task> GetDiagnosticsAsync( } } - private sealed class CodeAnalysisDiagnosticSource(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) : AbstractProjectDiagnosticSource(project) + private sealed class CodeAnalysisDiagnosticSource(Project project, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) + : AbstractProjectDiagnosticSource(project) { /// /// This source provides the results of the *last* explicitly kicked off "run code analysis" command from the @@ -66,7 +67,6 @@ public override bool IsLiveSource() => false; public override Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs index 4cc153fcc9739..e4712010ac803 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/AbstractWorkspaceDocumentDiagnosticSource.cs @@ -4,23 +4,33 @@ using System; using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal abstract class AbstractWorkspaceDocumentDiagnosticSource(TextDocument document) : AbstractDocumentDiagnosticSource(document) { - public static AbstractWorkspaceDocumentDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(TextDocument document, Func? shouldIncludeAnalyzer) - => new FullSolutionAnalysisDiagnosticSource(document, shouldIncludeAnalyzer); + public static AbstractWorkspaceDocumentDiagnosticSource CreateForFullSolutionAnalysisDiagnostics(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) + => new FullSolutionAnalysisDiagnosticSource(document, diagnosticAnalyzerService, shouldIncludeAnalyzer); public static AbstractWorkspaceDocumentDiagnosticSource CreateForCodeAnalysisDiagnostics(TextDocument document, ICodeAnalysisDiagnosticAnalyzerService codeAnalysisService) => new CodeAnalysisDiagnosticSource(document, codeAnalysisService); - private sealed class FullSolutionAnalysisDiagnosticSource(TextDocument document, Func? shouldIncludeAnalyzer) + private sealed class FullSolutionAnalysisDiagnosticSource(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) : AbstractWorkspaceDocumentDiagnosticSource(document) { + /// + /// Cached mapping between a project instance and all the diagnostics computed for it. This is used so that + /// once we compute the diagnostics once for a particular project, we don't need to recompute them again as we + /// walk every document within it. + /// + private static readonly ConditionalWeakTable>> s_projectToDiagnostics = new(); + /// /// This is a normal document source that represents live/fresh diagnostics that should supersede everything else. /// @@ -28,7 +38,6 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { @@ -40,14 +49,39 @@ public override async Task> GetDiagnosticsAsync( } else { - // We call GetDiagnosticsForIdsAsync as we want to ensure we get the full set of diagnostics for this document - // including those reported as a compilation end diagnostic. These are not included in document pull (uses GetDiagnosticsForSpan) due to cost. - // However we can include them as a part of workspace pull when FSA is on. - var documentDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( - Document.Project.Solution, Document.Project.Id, Document.Id, - diagnosticIds: null, shouldIncludeAnalyzer, includeSuppressedDiagnostics: false, - includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, cancellationToken).ConfigureAwait(false); - return documentDiagnostics; + var projectDiagnostics = await GetProjectDiagnosticsAsync(diagnosticAnalyzerService, cancellationToken).ConfigureAwait(false); + return projectDiagnostics.WhereAsArray(d => d.DocumentId == Document.Id); + } + } + + private async ValueTask> GetProjectDiagnosticsAsync( + IDiagnosticAnalyzerService diagnosticAnalyzerService, CancellationToken cancellationToken) + { + if (!s_projectToDiagnostics.TryGetValue(Document.Project, out var lazyDiagnostics)) + { + // Extracted into local to prevent captures. + lazyDiagnostics = GetLazyDiagnostics(); + } + + var result = await lazyDiagnostics.GetValueAsync(cancellationToken).ConfigureAwait(false); + return result[Document.Id].ToImmutableArray(); + + AsyncLazy> GetLazyDiagnostics() + { + return s_projectToDiagnostics.GetValue( + Document.Project, + _ => AsyncLazy.Create( + async cancellationToken => + { + var allDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( + Document.Project.Solution, Document.Project.Id, documentId: null, + diagnosticIds: null, shouldIncludeAnalyzer, + // Ensure we compute and return diagnostics for both the normal docs and the additional docs in this project. + static (project, _) => [.. project.DocumentIds.Concat(project.AdditionalDocumentIds)], + includeSuppressedDiagnostics: false, + includeLocalDocumentDiagnostics: true, includeNonLocalDocumentDiagnostics: true, cancellationToken).ConfigureAwait(false); + return allDiagnostics.Where(d => d.DocumentId != null).ToLookup(d => d.DocumentId!); + })); } } } @@ -64,7 +98,6 @@ public override bool IsLiveSource() => false; public override Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs index 54f5c6ce5deb2..e9a5ad49650bf 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/DocumentDiagnosticSource.cs @@ -7,11 +7,10 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.Copilot; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; -internal sealed class DocumentDiagnosticSource(DiagnosticKind diagnosticKind, TextDocument document) +internal sealed class DocumentDiagnosticSource(IDiagnosticAnalyzerService diagnosticAnalyzerService, DiagnosticKind diagnosticKind, TextDocument document) : AbstractDocumentDiagnosticSource(document) { public DiagnosticKind DiagnosticKind { get; } = diagnosticKind; @@ -23,14 +22,14 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + RequestContext context, CancellationToken cancellationToken) { // We call GetDiagnosticsForSpanAsync here instead of GetDiagnosticsForIdsAsync as it has faster perf // characteristics. GetDiagnosticsForIdsAsync runs analyzers against the entire compilation whereas // GetDiagnosticsForSpanAsync will only run analyzers against the request document. // Also ensure we pass in "includeSuppressedDiagnostics = true" for unnecessary suppressions to be reported. var allSpanDiagnostics = await diagnosticAnalyzerService.GetDiagnosticsForSpanAsync( - Document, range: null, diagnosticKind: this.DiagnosticKind, includeSuppressedDiagnostics: true, cancellationToken: cancellationToken).ConfigureAwait(false); + Document, range: null, diagnosticKind: this.DiagnosticKind, includeSuppressedDiagnostics: true, cancellationToken: cancellationToken).ConfigureAwait(false); // Add cached Copilot diagnostics when computing analyzer semantic diagnostics. // TODO: move to a separate diagnostic source. https://github.com/dotnet/roslyn/issues/72896 diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs index 34d110e4adb4c..e8cb70bc1de4c 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/IDiagnosticSource.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; using System.Threading.Tasks; @@ -33,7 +32,6 @@ internal interface IDiagnosticSource bool IsLiveSource(); Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken); } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs index 8ffd2d9b75fb9..d370f15240a60 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/NonLocalDocumentDiagnosticSource.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; -internal sealed class NonLocalDocumentDiagnosticSource(TextDocument document, Func? shouldIncludeAnalyzer) +internal sealed class NonLocalDocumentDiagnosticSource(TextDocument document, IDiagnosticAnalyzerService diagnosticAnalyzerService, Func? shouldIncludeAnalyzer) : AbstractDocumentDiagnosticSource(document) { private readonly Func? _shouldIncludeAnalyzer = shouldIncludeAnalyzer; @@ -19,7 +19,6 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) { diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs index df8a3491ccdcc..fe6ebc182c6f9 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DiagnosticSources/TaskListDiagnosticSource.cs @@ -33,7 +33,7 @@ public override bool IsLiveSource() => true; public override async Task> GetDiagnosticsAsync( - IDiagnosticAnalyzerService diagnosticAnalyzerService, RequestContext context, CancellationToken cancellationToken) + RequestContext context, CancellationToken cancellationToken) { var service = this.Document.GetLanguageService(); if (service == null) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs index eace41f6259c5..16ee2f87d2bf2 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandler.cs @@ -3,15 +3,10 @@ // See the LICENSE file in the project root for more information. using System.Collections.Immutable; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; -using Microsoft.CodeAnalysis.SolutionCrawler; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -21,13 +16,14 @@ internal partial class DocumentPullDiagnosticHandler { public DocumentPullDiagnosticHandler( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, diagnosticRefresher, globalOptions) + : base(analyzerService, diagnosticRefresher, diagnosticSourceManager, globalOptions) { } - protected override string? GetDiagnosticCategory(VSInternalDocumentDiagnosticsParams diagnosticsParams) + protected override string? GetRequestDiagnosticCategory(VSInternalDocumentDiagnosticsParams diagnosticsParams) => diagnosticsParams.QueryingDiagnosticKind?.Value; public override TextDocumentIdentifier? GetTextDocumentIdentifier(VSInternalDocumentDiagnosticsParams diagnosticsParams) @@ -72,59 +68,7 @@ protected override DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bo => ConvertTags(diagnosticData, isLiveSource, potentialDuplicate: false); protected override VSInternalDiagnosticReport[]? CreateReturn(BufferedProgress progress) - => progress.GetFlattenedValues(); - - protected override ValueTask> GetOrderedDiagnosticSourcesAsync( - VSInternalDocumentDiagnosticsParams diagnosticsParams, RequestContext context, CancellationToken cancellationToken) - => new(GetDiagnosticSource(diagnosticsParams, context) is { } diagnosticSource ? [diagnosticSource] : []); - - private IDiagnosticSource? GetDiagnosticSource(VSInternalDocumentDiagnosticsParams diagnosticsParams, RequestContext context) - { - var category = diagnosticsParams.QueryingDiagnosticKind?.Value; - - // TODO: Implement as extensibility point. https://github.com/dotnet/roslyn/issues/72896 - - if (category == PullDiagnosticCategories.Task) - return context.GetTrackedDocument() is { } document ? new TaskListDiagnosticSource(document, GlobalOptions) : null; - - if (category == PullDiagnosticCategories.EditAndContinue) - return GetEditAndContinueDiagnosticSource(context); - - var diagnosticKind = category switch - { - PullDiagnosticCategories.DocumentCompilerSyntax => DiagnosticKind.CompilerSyntax, - PullDiagnosticCategories.DocumentCompilerSemantic => DiagnosticKind.CompilerSemantic, - PullDiagnosticCategories.DocumentAnalyzerSyntax => DiagnosticKind.AnalyzerSyntax, - PullDiagnosticCategories.DocumentAnalyzerSemantic => DiagnosticKind.AnalyzerSemantic, - // if this request doesn't have a category at all (legacy behavior, assume they're asking about everything). - null => DiagnosticKind.All, - // if it's a category we don't recognize, return nothing. - _ => (DiagnosticKind?)null, - }; - - if (diagnosticKind is null) - return null; - - return GetDiagnosticSource(diagnosticKind.Value, context); - } - - internal static IDiagnosticSource? GetEditAndContinueDiagnosticSource(RequestContext context) - => context.GetTrackedDocument() is { } document ? EditAndContinueDiagnosticSource.CreateOpenDocumentSource(document) : null; - - internal static IDiagnosticSource? GetDiagnosticSource(DiagnosticKind diagnosticKind, RequestContext context) - => context.GetTrackedDocument() is { } textDocument ? new DocumentDiagnosticSource(diagnosticKind, textDocument) : null; - - internal static IDiagnosticSource? GetNonLocalDiagnosticSource(RequestContext context, IGlobalOptionService globalOptions) { - var textDocument = context.GetTrackedDocument(); - if (textDocument == null) - return null; - - // Non-local document diagnostics are reported only when full solution analysis is enabled for analyzer execution. - if (globalOptions.GetBackgroundAnalysisScope(textDocument.Project.Language) != BackgroundAnalysisScope.FullSolution) - return null; - - // NOTE: Compiler does not report any non-local diagnostics, so we bail out for compiler analyzer. - return new NonLocalDocumentDiagnosticSource(textDocument, shouldIncludeAnalyzer: static analyzer => !analyzer.IsCompilerAnalyzer()); + return progress.GetFlattenedValues(); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs index d731fe70276cd..07a6cfcaabc17 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/DocumentPullDiagnosticHandlerFactory.cs @@ -6,6 +6,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -14,6 +15,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal class DocumentPullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; + private readonly IDiagnosticSourceManager _diagnosticSourceManager; private readonly IDiagnosticsRefresher _diagnosticsRefresher; private readonly IGlobalOptionService _globalOptions; @@ -21,14 +23,16 @@ internal class DocumentPullDiagnosticHandlerFactory : ILspServiceFactory [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public DocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; + _diagnosticSourceManager = diagnosticSourceManager; _diagnosticsRefresher = diagnosticsRefresher; _globalOptions = globalOptions; } public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) - => new DocumentPullDiagnosticHandler(_analyzerService, _diagnosticsRefresher, _globalOptions); + => new DocumentPullDiagnosticHandler(_analyzerService, _diagnosticSourceManager, _diagnosticsRefresher, _globalOptions); } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentNonLocalDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentNonLocalDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..9b8f9116ebc19 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentNonLocalDiagnosticSourceProvider.cs @@ -0,0 +1,41 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.SolutionCrawler; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class PublicDocumentNonLocalDiagnosticSourceProvider( + [Import] IGlobalOptionService globalOptions, + [Import] IDiagnosticAnalyzerService diagnosticAnalyzerService) + : IDiagnosticSourceProvider +{ + public const string NonLocal = nameof(NonLocal); + public bool IsDocument => true; + public string Name => NonLocal; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + // Non-local document diagnostics are reported only when full solution analysis is enabled for analyzer execution. + if (context.GetTrackedDocument() is { } textDocument && + globalOptions.GetBackgroundAnalysisScope(textDocument.Project.Language) == BackgroundAnalysisScope.FullSolution) + { + // NOTE: Compiler does not report any non-local diagnostics, so we bail out for compiler analyzer. + return new([new NonLocalDocumentDiagnosticSource(textDocument, diagnosticAnalyzerService, a => !a.IsCompilerAnalyzer())]); + } + + return new([]); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs index e369e4180ed43..3ae460c780e84 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticHandlerFactory.cs @@ -5,10 +5,9 @@ using System; using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; -using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; @@ -19,6 +18,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; internal sealed class PublicDocumentPullDiagnosticHandlerFactory : ILspServiceFactory { private readonly IDiagnosticAnalyzerService _analyzerService; + private readonly IDiagnosticSourceManager _diagnosticSourceManager; private readonly IDiagnosticsRefresher _diagnosticRefresher; private readonly IGlobalOptionService _globalOptions; @@ -26,10 +26,12 @@ internal sealed class PublicDocumentPullDiagnosticHandlerFactory : ILspServiceFa [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public PublicDocumentPullDiagnosticHandlerFactory( IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticRefresher, IGlobalOptionService globalOptions) { _analyzerService = analyzerService; + _diagnosticSourceManager = diagnosticSourceManager; _diagnosticRefresher = diagnosticRefresher; _globalOptions = globalOptions; } @@ -37,6 +39,6 @@ public PublicDocumentPullDiagnosticHandlerFactory( public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var clientLanguageServerManager = lspServices.GetRequiredService(); - return new PublicDocumentPullDiagnosticsHandler(clientLanguageServerManager, _analyzerService, _diagnosticRefresher, _globalOptions); + return new PublicDocumentPullDiagnosticsHandler(clientLanguageServerManager, _analyzerService, _diagnosticSourceManager, _diagnosticRefresher, _globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs index 400087cd41ef7..426148bed2f11 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler.cs @@ -2,52 +2,42 @@ // 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.Linq; -using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Roslyn.LanguageServer.Protocol; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; -using DocumentDiagnosticReport = SumType; - // A document diagnostic partial report is defined as having the first literal send = DocumentDiagnosticReport (aka changed / unchanged) followed // by n DocumentDiagnosticPartialResult literals. -// See https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/proposed.diagnostics.md#textDocument_diagnostic +// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentDiagnosticParams using DocumentDiagnosticPartialReport = SumType; +using DocumentDiagnosticReport = SumType; [Method(Methods.TextDocumentDiagnosticName)] internal sealed partial class PublicDocumentPullDiagnosticsHandler : AbstractDocumentPullDiagnosticHandler { - private readonly string _nonLocalDiagnosticsSourceRegistrationId; private readonly IClientLanguageServerManager _clientLanguageServerManager; public PublicDocumentPullDiagnosticsHandler( IClientLanguageServerManager clientLanguageServerManager, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) - : base(analyzerService, diagnosticsRefresher, globalOptions) + : base(analyzerService, diagnosticsRefresher, diagnosticSourceManager, globalOptions) { - _nonLocalDiagnosticsSourceRegistrationId = Guid.NewGuid().ToString(); _clientLanguageServerManager = clientLanguageServerManager; } - /// - /// Public API doesn't support categories (yet). - /// - protected override string? GetDiagnosticCategory(DocumentDiagnosticParams diagnosticsParams) - => null; + protected override string? GetRequestDiagnosticCategory(DocumentDiagnosticParams diagnosticsParams) + => diagnosticsParams.Identifier; - public override TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams diagnosticsParams) => diagnosticsParams.TextDocument; - - protected override string? GetDiagnosticSourceIdentifier(DocumentDiagnosticParams diagnosticsParams) => diagnosticsParams.Identifier; + public override TextDocumentIdentifier GetTextDocumentIdentifier(DocumentDiagnosticParams diagnosticsParams) + => diagnosticsParams.TextDocument; protected override DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource) => ConvertTags(diagnosticData, isLiveSource, potentialDuplicate: false); @@ -92,18 +82,6 @@ protected override bool TryCreateUnchangedReport(TextDocumentIdentifier identifi return null; } - protected override ValueTask> GetOrderedDiagnosticSourcesAsync(DocumentDiagnosticParams diagnosticParams, RequestContext context, CancellationToken cancellationToken) - { - var nonLocalDocumentDiagnostics = diagnosticParams.Identifier == DocumentNonLocalDiagnosticIdentifier.ToString(); - - // Task list items are not reported through the public LSP diagnostic API. - var source = nonLocalDocumentDiagnostics - ? DocumentPullDiagnosticHandler.GetNonLocalDiagnosticSource(context, GlobalOptions) - : DocumentPullDiagnosticHandler.GetDiagnosticSource(DiagnosticKind.All, context); - - return new(source != null ? [source] : []); - } - protected override ImmutableArray? GetPreviousResults(DocumentDiagnosticParams diagnosticsParams) { if (diagnosticsParams.PreviousResultId != null && diagnosticsParams.TextDocument != null) diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs index 940723c0e652a..a8332219d1d12 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicDocumentPullDiagnosticsHandler_IOnInitialized.cs @@ -2,61 +2,45 @@ // 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.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.SolutionCrawler; using Roslyn.LanguageServer.Protocol; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; - -using DocumentDiagnosticReport = SumType; - // A document diagnostic partial report is defined as having the first literal send = DocumentDiagnosticReport (aka changed / unchanged) followed // by n DocumentDiagnosticPartialResult literals. // See https://github.com/microsoft/vscode-languageserver-node/blob/main/protocol/src/common/proposed.diagnostics.md#textDocument_diagnostic -using DocumentDiagnosticPartialReport = SumType; -internal sealed partial class PublicDocumentPullDiagnosticsHandler : AbstractDocumentPullDiagnosticHandler, IOnInitialized +internal sealed partial class PublicDocumentPullDiagnosticsHandler : IOnInitialized { public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { - // Dynamically register a non-local document diagnostic source if Full solution background analysis is enabled - // for analyzer execution. This diagnostic source reports diagnostics in open documents that are reported - // when analyzing other documents or at compilation end. - if (clientCapabilities?.TextDocument?.Diagnostic?.DynamicRegistration is true && IsFsaEnabled()) + // Dynamically register for all of our document diagnostic sources. + if (clientCapabilities?.TextDocument?.Diagnostic?.DynamicRegistration is true) { // TODO: Hookup an option changed handler for changes to BackgroundAnalysisScopeOption // to dynamically register/unregister the non-local document diagnostic source. + // Task diagnostics shouldn't be reported through VSCode (it has its own task stuff). Additional cleanup needed. + var sources = DiagnosticSourceManager.GetDocumentSourceProviderNames().Where(source => source != PullDiagnosticCategories.Task); + var registrations = sources.Select(FromSourceName).ToArray(); await _clientLanguageServerManager.SendRequestAsync( methodName: Methods.ClientRegisterCapabilityName, @params: new RegistrationParams() { - Registrations = - [ - new Registration - { - Id = _nonLocalDiagnosticsSourceRegistrationId, - Method = Methods.TextDocumentDiagnosticName, - RegisterOptions = new DiagnosticRegistrationOptions - { - Identifier = DocumentNonLocalDiagnosticIdentifier.ToString() - } - } - ] + Registrations = registrations }, cancellationToken).ConfigureAwait(false); } - bool IsFsaEnabled() - { - foreach (var language in context.SupportedLanguages) + Registration FromSourceName(string sourceName) + => new() { - if (GlobalOptions.GetBackgroundAnalysisScope(language) == BackgroundAnalysisScope.FullSolution) - return true; - } - - return false; - } + Id = Guid.NewGuid().ToString(), + Method = Methods.TextDocumentDiagnosticName, + RegisterOptions = new DiagnosticRegistrationOptions { Identifier = sourceName } + }; } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs index 90970a521fe53..9e11c966f21c4 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticHandlerFactory.cs @@ -6,6 +6,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; @@ -16,12 +17,14 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; internal sealed class PublicWorkspacePullDiagnosticHandlerFactory( LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var workspaceManager = lspServices.GetRequiredService(); - return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, registrationService, analyzerService, diagnosticsRefresher, globalOptions); + var clientLanguageServerManager = lspServices.GetRequiredService(); + return new PublicWorkspacePullDiagnosticsHandler(workspaceManager, registrationService, clientLanguageServerManager, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs index 6c2beb5517041..15cf5adac2d83 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler.cs @@ -7,6 +7,7 @@ using System.Linq; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; @@ -19,21 +20,24 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; using WorkspaceDiagnosticPartialReport = SumType; [Method(Methods.WorkspaceDiagnosticName)] -internal sealed class PublicWorkspacePullDiagnosticsHandler( - LspWorkspaceManager workspaceManager, - LspWorkspaceRegistrationService registrationService, - IDiagnosticAnalyzerService analyzerService, - IDiagnosticsRefresher diagnosticRefresher, - IGlobalOptionService globalOptions) - : AbstractWorkspacePullDiagnosticsHandler( - workspaceManager, registrationService, analyzerService, diagnosticRefresher, globalOptions), IDisposable +internal sealed partial class PublicWorkspacePullDiagnosticsHandler : AbstractWorkspacePullDiagnosticsHandler, IDisposable { + private readonly IClientLanguageServerManager _clientLanguageServerManager; + public PublicWorkspacePullDiagnosticsHandler( + LspWorkspaceManager workspaceManager, + LspWorkspaceRegistrationService registrationService, + IClientLanguageServerManager clientLanguageServerManager, + IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, + IDiagnosticsRefresher diagnosticRefresher, + IGlobalOptionService globalOptions) + : base(workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticRefresher, globalOptions) + { + _clientLanguageServerManager = clientLanguageServerManager; + } - /// - /// Public API doesn't support categories (yet). - /// - protected override string? GetDiagnosticCategory(WorkspaceDiagnosticParams diagnosticsParams) - => null; + protected override string? GetRequestDiagnosticCategory(WorkspaceDiagnosticParams diagnosticsParams) + => diagnosticsParams.Identifier; protected override DiagnosticTag[] ConvertTags(DiagnosticData diagnosticData, bool isLiveSource) => ConvertTags(diagnosticData, isLiveSource, potentialDuplicate: false); diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler_IOnInitialized.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler_IOnInitialized.cs new file mode 100644 index 0000000000000..2e34f2dbccf03 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/Public/PublicWorkspacePullDiagnosticsHandler_IOnInitialized.cs @@ -0,0 +1,35 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Public; + +internal sealed partial class PublicWorkspacePullDiagnosticsHandler : IOnInitialized +{ + public async Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + if (clientCapabilities?.TextDocument?.Diagnostic?.DynamicRegistration is true) + { + var providerNames = DiagnosticSourceManager.GetWorkspaceSourceProviderNames(); + await _clientLanguageServerManager.SendRequestAsync( + methodName: Methods.ClientRegisterCapabilityName, + @params: new RegistrationParams + { + Registrations = providerNames.Select(name => new Registration + { + // Due to https://github.com/microsoft/language-server-protocol/issues/1723 + // we need to use textDocument/diagnostic instead of workspace/diagnostic + Method = Methods.TextDocumentDiagnosticName, + Id = name, + RegisterOptions = new DiagnosticRegistrationOptions { Identifier = name, InterFileDependencies = true, WorkDoneProgress = true } + }).ToArray() + }, + cancellationToken).ConfigureAwait(false); + } + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs index 66893cacf32fb..62f6e9b5b8332 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandler.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; using Roslyn.LanguageServer.Protocol; using Roslyn.Utilities; @@ -17,26 +18,27 @@ internal sealed partial class WorkspacePullDiagnosticHandler( LspWorkspaceManager workspaceManager, LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) : AbstractWorkspacePullDiagnosticsHandler( - workspaceManager, registrationService, analyzerService, diagnosticsRefresher, globalOptions) + workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions) { - protected override string? GetDiagnosticCategory(VSInternalWorkspaceDiagnosticsParams diagnosticsParams) + protected override string? GetRequestDiagnosticCategory(VSInternalWorkspaceDiagnosticsParams diagnosticsParams) => diagnosticsParams.QueryingDiagnosticKind?.Value; protected override VSInternalWorkspaceDiagnosticReport[] CreateReport(TextDocumentIdentifier identifier, Roslyn.LanguageServer.Protocol.Diagnostic[]? diagnostics, string? resultId) - => [ - new VSInternalWorkspaceDiagnosticReport - { - TextDocument = identifier, - Diagnostics = diagnostics, - ResultId = resultId, - // Mark these diagnostics as having come from us. They will be superseded by any diagnostics for the - // same file produced by the DocumentPullDiagnosticHandler. - Identifier = WorkspaceDiagnosticIdentifier, - } - ]; + => [ + new VSInternalWorkspaceDiagnosticReport + { + TextDocument = identifier, + Diagnostics = diagnostics, + ResultId = resultId, + // Mark these diagnostics as having come from us. They will be superseded by any diagnostics for the + // same file produced by the DocumentPullDiagnosticHandler. + Identifier = WorkspaceDiagnosticIdentifier, + } + ]; protected override VSInternalWorkspaceDiagnosticReport[] CreateRemovedReport(TextDocumentIdentifier identifier) => CreateReport(identifier, diagnostics: null, resultId: null); diff --git a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs index b928cf51f3910..7c13f4d46b74a 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Diagnostics/WorkspacePullDiagnosticHandlerFactory.cs @@ -6,6 +6,7 @@ using System.Composition; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.DiagnosticSources; using Microsoft.CodeAnalysis.Options; namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; @@ -16,12 +17,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; internal class WorkspacePullDiagnosticHandlerFactory( LspWorkspaceRegistrationService registrationService, IDiagnosticAnalyzerService analyzerService, + IDiagnosticSourceManager diagnosticSourceManager, IDiagnosticsRefresher diagnosticsRefresher, IGlobalOptionService globalOptions) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var workspaceManager = lspServices.GetRequiredService(); - return new WorkspacePullDiagnosticHandler(workspaceManager, registrationService, analyzerService, diagnosticsRefresher, globalOptions); + return new WorkspacePullDiagnosticHandler(workspaceManager, registrationService, analyzerService, diagnosticSourceManager, diagnosticsRefresher, globalOptions); } } diff --git a/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/DocumentEditAndContinueDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/DocumentEditAndContinueDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..63e5866c67b1e --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/DocumentEditAndContinueDiagnosticSourceProvider.cs @@ -0,0 +1,32 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DocumentEditAndContinueDiagnosticSourceProvider() : IDiagnosticSourceProvider +{ + public bool IsDocument => true; + public string Name => PullDiagnosticCategories.EditAndContinue; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.GetTrackedDocument() is { } document) + { + return new([EditAndContinueDiagnosticSource.CreateOpenDocumentSource(document)]); + } + + return new([]); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/WorkspaceEditAndContinueDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/WorkspaceEditAndContinueDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..847a909a2ff25 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/EditAndContinue/WorkspaceEditAndContinueDiagnosticSourceProvider.cs @@ -0,0 +1,29 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.EditAndContinue; +using Microsoft.CodeAnalysis.Host.Mef; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceEditAndContinueDiagnosticSourceProvider() : IDiagnosticSourceProvider +{ + public bool IsDocument => false; + public string Name => PullDiagnosticCategories.EditAndContinue; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + return EditAndContinueDiagnosticSource.CreateWorkspaceDiagnosticSourcesAsync(context.Solution!, document => context.IsTracking(document.GetURI()), cancellationToken); + } +} diff --git a/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs b/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs index 176c3f17b0739..806d070ff0463 100644 --- a/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/Highlights/DocumentHighlightHandler.cs @@ -53,14 +53,14 @@ public DocumentHighlightsHandler(IHighlightingService highlightingService, IGlob var keywordHighlights = await GetKeywordHighlightsAsync(document, text, position, cancellationToken).ConfigureAwait(false); if (keywordHighlights.Any()) { - return keywordHighlights.ToArray(); + return [.. keywordHighlights]; } // Not a keyword, check if it is a reference that needs highlighting. var referenceHighlights = await GetReferenceHighlightsAsync(document, text, position, cancellationToken).ConfigureAwait(false); if (referenceHighlights.Any()) { - return referenceHighlights.ToArray(); + return [.. referenceHighlights]; } // No keyword or references to highlight at this location. diff --git a/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs b/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs index a488aefea3f12..7ea64d0c5d474 100644 --- a/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/OnAutoInsert/OnAutoInsertHandler.cs @@ -166,7 +166,7 @@ public OnAutoInsertHandler( var indentedText = GetIndentedText(newSourceText, caretLine, desiredCaretLinePosition, options); // Get the overall text changes between the original text and the formatted + indented text. - textChanges = indentedText.GetTextChanges(sourceText).ToImmutableArray(); + textChanges = [.. indentedText.GetTextChanges(sourceText)]; newSourceText = indentedText; // If tabs were inserted the desired caret column can remain beyond the line text. diff --git a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs index 1fd14087e596d..ee897994efe61 100644 --- a/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs +++ b/src/Features/LanguageServer/Protocol/Handler/ProjectContext/GetTextDocumentWithContextHandler.cs @@ -63,7 +63,7 @@ public GetTextDocumentWithContextHandler() return Task.FromResult(new VSProjectContextList { - ProjectContexts = contexts.ToArray(), + ProjectContexts = [.. contexts], DefaultIndex = documentIds.IndexOf(d => d == currentContextDocumentId) }); } diff --git a/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs b/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs index 2338d5b383c31..9e57963e23ef3 100644 --- a/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs +++ b/src/Features/LanguageServer/Protocol/Handler/References/FindUsagesLSPContext.cs @@ -259,7 +259,7 @@ public override async ValueTask OnReferenceFoundAsync(SourceReferenceItem refere var docText = await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); var classifiedTextRuns = GetClassifiedTextRuns(_id, definitionId, documentSpan.Value, isWrittenTo, classifiedSpans, docText); - return new ClassifiedTextElement(classifiedTextRuns.ToArray()); + return new ClassifiedTextElement([.. classifiedTextRuns]); } // Certain definitions may not have a DocumentSpan, such as namespace and metadata definitions @@ -317,7 +317,7 @@ private static ClassifiedTextRun[] GetClassifiedTextRuns( private ValueTask ReportReferencesAsync(ImmutableSegmentedList> referencesToReport, CancellationToken cancellationToken) { // We can report outside of the lock here since _progress is thread-safe. - _progress.Report(referencesToReport.ToArray()); + _progress.Report([.. referencesToReport]); return ValueTaskFactory.CompletedTask; } } diff --git a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs index 752ec12df2eeb..03c19b8338212 100644 --- a/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs +++ b/src/Features/LanguageServer/Protocol/Handler/SemanticTokens/SemanticTokensSchema.cs @@ -102,7 +102,7 @@ public SemanticTokensSchema(IReadOnlyDictionary tokenTypeMap) .Order() .ToImmutableArray(); - AllTokenTypes = SemanticTokenTypes.AllTypes.Concat(customTokenTypes).ToImmutableArray(); + AllTokenTypes = [.. SemanticTokenTypes.AllTypes, .. customTokenTypes]; var tokenTypeToIndex = new Dictionary(); diff --git a/src/Features/LanguageServer/Protocol/Handler/Tasks/DocumentTaskDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Tasks/DocumentTaskDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..6b63bdf91f220 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Tasks/DocumentTaskDiagnosticSourceProvider.cs @@ -0,0 +1,33 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Tasks; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DocumentTaskDiagnosticSourceProvider([Import] IGlobalOptionService globalOptions) : IDiagnosticSourceProvider +{ + public bool IsDocument => true; + public string Name => PullDiagnosticCategories.Task; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.GetTrackedDocument() is { } document) + { + return new([new TaskListDiagnosticSource(document, globalOptions)]); + } + + return new([]); + } +} + diff --git a/src/Features/LanguageServer/Protocol/Handler/Tasks/WorkspaceTaskDiagnosticSourceProvider.cs b/src/Features/LanguageServer/Protocol/Handler/Tasks/WorkspaceTaskDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..53b1bf7e03cd5 --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Handler/Tasks/WorkspaceTaskDiagnosticSourceProvider.cs @@ -0,0 +1,48 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Options; +using Microsoft.CodeAnalysis.PooledObjects; +using Microsoft.CodeAnalysis.TaskList; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.Tasks; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class WorkspaceTaskDiagnosticSourceProvider([Import] IGlobalOptionService globalOptions) : IDiagnosticSourceProvider +{ + public bool IsDocument => false; + public string Name => PullDiagnosticCategories.Task; + + public ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + Contract.ThrowIfNull(context.Solution); + + // Only compute task list items for closed files if the option is on for it. + if (globalOptions.GetTaskListOptions().ComputeForClosedFiles) + { + using var _ = ArrayBuilder.GetInstance(out var result); + foreach (var project in WorkspaceDiagnosticSourceHelpers.GetProjectsInPriorityOrder(context.Solution, context.SupportedLanguages)) + { + foreach (var document in project.Documents) + { + if (!WorkspaceDiagnosticSourceHelpers.ShouldSkipDocument(context, document)) + result.Add(new TaskListDiagnosticSource(document, globalOptions)); + } + } + + return new(result.ToImmutableAndClear()); + } + + return new([]); + } +} diff --git a/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs b/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs index f604d0049d19f..099a38a5f3889 100644 --- a/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs +++ b/src/Features/LanguageServer/Protocol/LspServices/LspServices.cs @@ -153,7 +153,7 @@ public void Dispose() ImmutableArray disposableServices; lock (_gate) { - disposableServices = _servicesToDispose.ToImmutableArray(); + disposableServices = [.. _servicesToDispose]; _servicesToDispose.Clear(); } diff --git a/src/Features/LanguageServer/Protocol/Protocol/Internal/Converters/VSCodeInternalExtensionUtilities.cs b/src/Features/LanguageServer/Protocol/Protocol/Internal/Converters/VSCodeInternalExtensionUtilities.cs new file mode 100644 index 0000000000000..a1d482e7a480a --- /dev/null +++ b/src/Features/LanguageServer/Protocol/Protocol/Internal/Converters/VSCodeInternalExtensionUtilities.cs @@ -0,0 +1,52 @@ +// 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 Roslyn.LanguageServer.Protocol +{ + using Newtonsoft.Json; + + /// + /// Utilities to aid work with VS Code LSP Extensions. + /// + internal static class VSCodeInternalExtensionUtilities + { + /// + /// Adds necessary to deserialize + /// JSON stream into objects which include VS Code-specific extensions. + /// + /// + /// If is used in parallel to execution of this method, + /// its access needs to be synchronized with this method call, to guarantee that + /// collection is not modified when in use. + /// + /// Instance of which is guaranteed to not work in parallel to this method call. + public static void AddVSCodeInternalExtensionConverters(this JsonSerializer serializer) + { + // Reading the number of converters before we start adding new ones + var existingConvertersCount = serializer.Converters.Count; + + AddOrReplaceConverter(); + AddOrReplaceConverter(); + + void AddOrReplaceConverter() + where TExtension : TBase + { + for (var i = 0; i < existingConvertersCount; i++) + { + var existingConverterType = serializer.Converters[i].GetType(); + if (existingConverterType.IsGenericType && + existingConverterType.GetGenericTypeDefinition() == typeof(VSExtensionConverter<,>) && + existingConverterType.GenericTypeArguments[0] == typeof(TBase)) + { + serializer.Converters.RemoveAt(i); + existingConvertersCount--; + break; + } + } + + serializer.Converters.Add(new VSExtensionConverter()); + } + } + } +} diff --git a/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs b/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs index 3dc9a54510650..8570c4d0db31b 100644 --- a/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs +++ b/src/Features/LanguageServer/Protocol/RoslynLanguageServer.cs @@ -39,6 +39,8 @@ public RoslynLanguageServer( _lspServiceProvider = lspServiceProvider; _serverKind = serverKind; + VSCodeInternalExtensionUtilities.AddVSCodeInternalExtensionConverters(serializer); + // Create services that require base dependencies (jsonrpc) or are more complex to create to the set manually. _baseServices = GetBaseServices(jsonRpc, logger, capabilitiesProvider, hostServices, serverKind, supportedLanguages); @@ -54,7 +56,7 @@ protected override ILspServices ConstructLspServices() protected override IRequestExecutionQueue ConstructRequestExecutionQueue() { var provider = GetLspServices().GetRequiredService>(); - return provider.CreateRequestExecutionQueue(this, _logger, HandlerProvider); + return provider.CreateRequestExecutionQueue(this, Logger, HandlerProvider); } private ImmutableDictionary>> GetBaseServices( @@ -115,7 +117,7 @@ protected override string GetLanguageForRequest(string methodName, JToken? param { if (parameters == null) { - _logger.LogInformation("No request parameters given, using default language handler"); + Logger.LogInformation("No request parameters given, using default language handler"); return LanguageServerConstants.DefaultLanguageName; } @@ -140,7 +142,7 @@ protected override string GetLanguageForRequest(string methodName, JToken? param var uri = uriToken.ToObject(_jsonSerializer); Contract.ThrowIfNull(uri, "Failed to deserialize uri property"); var language = lspWorkspaceManager.GetLanguageForUri(uri); - _logger.LogInformation($"Using {language} from request text document"); + Logger.LogInformation($"Using {language} from request text document"); return language; } @@ -154,12 +156,12 @@ protected override string GetLanguageForRequest(string methodName, JToken? param var data = dataToken.ToObject(_jsonSerializer); Contract.ThrowIfNull(data, "Failed to document resolve data object"); var language = lspWorkspaceManager.GetLanguageForUri(data.TextDocument.Uri); - _logger.LogInformation($"Using {language} from data text document"); + Logger.LogInformation($"Using {language} from data text document"); return language; } // This request is not for a textDocument and is not a resolve request. - _logger.LogInformation("Request did not contain a textDocument, using default language handler"); + Logger.LogInformation("Request did not contain a textDocument, using default language handler"); return LanguageServerConstants.DefaultLanguageName; static bool ShouldUseDefaultLanguage(string methodName) diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs index 201f98f4233cf..73231c4dc1bf1 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Diagnostics/AbstractPullDiagnosticTestsBase.cs @@ -303,7 +303,7 @@ static DocumentDiagnosticParams CreateProposedDocumentDiagnosticParams( { return new DocumentDiagnosticParams { - Identifier = testNonLocalDiagnostics ? DocumentPullDiagnosticHandler.DocumentNonLocalDiagnosticIdentifier.ToString() : null, + Identifier = testNonLocalDiagnostics ? PublicDocumentNonLocalDiagnosticSourceProvider.NonLocal : null, PreviousResultId = previousResultId, PartialResultToken = progress, TextDocument = vsTextDocumentIdentifier, diff --git a/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs b/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs index ed5bbf322c5e3..f4abf45dec3ce 100644 --- a/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs +++ b/src/Features/LanguageServer/ProtocolUnitTests/Symbols/DocumentSymbolsTests.cs @@ -181,7 +181,7 @@ private static LSP.DocumentSymbol CreateDocumentSymbol(LSP.SymbolKind kind, stri { var children = parent.Children.ToList(); children.Add(documentSymbol); - parent.Children = children.ToArray(); + parent.Children = [.. children]; } return documentSymbol; diff --git a/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs b/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs index a6c802e7ab9ee..3534e172218e2 100644 --- a/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs +++ b/src/Features/Test/EditAndContinue/EditAndContinueMethodDebugInfoReaderTests.cs @@ -110,7 +110,7 @@ public static void Main() Assert.False(reader.TryGetDocumentChecksum("/A/C.cs", out _, out _)); Assert.True(reader.TryGetDocumentChecksum("/a/c.cs", out var actualChecksum, out var actualAlgorithm)); - Assert.Equal("21-C8-B2-D7-A3-6B-49-C7-57-DF-67-B8-1F-75-DF-6A-64-FD-59-22", BitConverter.ToString(actualChecksum.ToArray())); + Assert.Equal("21-C8-B2-D7-A3-6B-49-C7-57-DF-67-B8-1F-75-DF-6A-64-FD-59-22", BitConverter.ToString([.. actualChecksum])); Assert.Equal(new Guid("ff1816ec-aa5e-4d10-87f7-6f4963833460"), actualAlgorithm); } } diff --git a/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs b/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs index 8419a40314bd9..f5767ebb02d68 100644 --- a/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs +++ b/src/Features/TestUtilities/Diagnostics/TestDiagnosticAnalyzerDriver.cs @@ -55,7 +55,9 @@ private async Task> GetDiagnosticsAsync( if (getDocumentDiagnostics) { var text = await document.GetTextAsync().ConfigureAwait(false); - var dxs = await _diagnosticAnalyzerService.GetDiagnosticsAsync(project.Solution, project.Id, document.Id, _includeSuppressedDiagnostics, _includeNonLocalDocumentDiagnostics, CancellationToken.None); + var dxs = await _diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, document.Id, diagnosticIds: null, shouldIncludeAnalyzer: null, + _includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: true, _includeNonLocalDocumentDiagnostics, CancellationToken.None); documentDiagnostics = await CodeAnalysis.Diagnostics.Extensions.ToDiagnosticsAsync( filterSpan is null ? dxs.Where(d => d.DataLocation.DocumentId != null) @@ -66,7 +68,9 @@ filterSpan is null if (getProjectDiagnostics) { - var dxs = await _diagnosticAnalyzerService.GetDiagnosticsAsync(project.Solution, project.Id, documentId: null, _includeSuppressedDiagnostics, _includeNonLocalDocumentDiagnostics, CancellationToken.None); + var dxs = await _diagnosticAnalyzerService.GetDiagnosticsForIdsAsync( + project.Solution, project.Id, documentId: null, diagnosticIds: null, shouldIncludeAnalyzer: null, + _includeSuppressedDiagnostics, includeLocalDocumentDiagnostics: true, _includeNonLocalDocumentDiagnostics, CancellationToken.None); projectDiagnostics = await CodeAnalysis.Diagnostics.Extensions.ToDiagnosticsAsync(dxs.Where(d => d.DocumentId is null), project, CancellationToken.None); } diff --git a/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs b/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs index 916f551d29251..ec33eac92f782 100644 --- a/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs +++ b/src/Features/TestUtilities/EditAndContinue/ActiveStatementsDescription.cs @@ -100,7 +100,7 @@ internal static ImmutableArray CreateActiveStatementMap var activeStatementMarkers = SourceMarkers.GetActiveSpans(markedSource).ToArray(); var exceptionRegionMarkers = SourceMarkers.GetExceptionRegions(markedSource); - return activeStatementMarkers.Aggregate( + return [.. activeStatementMarkers.Aggregate( new List(), (list, marker) => { @@ -126,7 +126,7 @@ internal static ImmutableArray CreateActiveStatementMap documentActiveStatements.Add(unmappedActiveStatement.Statement); return SourceMarkers.SetListItem(list, ordinal, unmappedActiveStatement); - }).ToImmutableArray(); + })]; } internal static ImmutableArray GetUnmappedActiveStatements( diff --git a/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs b/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs index 0efea7cc9ab11..1c43208de1796 100644 --- a/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs +++ b/src/Features/TestUtilities/EditAndContinue/SourceMarkers.cs @@ -106,7 +106,7 @@ public static (int id, TextSpan span)[] GetTrackingSpans(string src) Contract.ThrowIfTrue(result.Any(span => span == default)); - return result.ToArray(); + return [.. result]; } public static ImmutableArray> GetExceptionRegions(string markedSource) diff --git a/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs b/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs index aa8265e7d4dbb..7a20c142784b2 100644 --- a/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs +++ b/src/Tools/ExternalAccess/Copilot/Internal/CodeMapper/CopilotCSharpMapCodeService.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.ExternalAccess.Copilot.CodeMapper; @@ -16,8 +15,6 @@ namespace Microsoft.CodeAnalysis.ExternalAccess.Copilot.Internal.CodeMapper; -using MapCodeAsyncDelegateType = Func, ImmutableArray<(Document Document, TextSpan TextSpan)>, Dictionary, CancellationToken, Task?>>; - [ExportLanguageService(typeof(IMapCodeService), language: LanguageNames.CSharp), Shared] internal sealed class CSharpMapCodeService : IMapCodeService { @@ -25,9 +22,9 @@ internal sealed class CSharpMapCodeService : IMapCodeService [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public CSharpMapCodeService([Import(AllowDefault = true)] ICSharpCopilotMapCodeService? service) + public CSharpMapCodeService(ICSharpCopilotMapCodeService service) { - _service = service ?? new ReflectionWrapper(); + _service = service; } public Task?> MapCodeAsync(Document document, ImmutableArray contents, ImmutableArray<(Document, TextSpan)> focusLocations, CancellationToken cancellationToken) @@ -35,49 +32,4 @@ public CSharpMapCodeService([Import(AllowDefault = true)] ICSharpCopilotMapCodeS var options = new Dictionary(); return _service.MapCodeAsync(document, contents, focusLocations, options, cancellationToken); } - - private sealed class ReflectionWrapper : ICSharpCopilotMapCodeService - { - private const string CodeMapperDllName = "Microsoft.VisualStudio.Copilot.CodeMappers.CSharp, Version=0.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"; - private const string MapCodeServiceTypeFullName = "Microsoft.VisualStudio.Conversations.CodeMappers.CSharp.CSharpMapCodeService"; - private const string MapCodeAsyncMethodName = "MapCodeAsync"; - - // Create and cache the delegate to ensure we use a singleton and better performance. - private readonly Lazy _lazyMapCodeAsyncDelegate = new(CreateDelegate, LazyThreadSafetyMode.PublicationOnly); - - private static MapCodeAsyncDelegateType? CreateDelegate() - { - try - { - var assembly = Assembly.Load(CodeMapperDllName); - var type = assembly.GetType(MapCodeServiceTypeFullName); - if (type is null) - return null; - - var serviceInstance = Activator.CreateInstance(type); - if (serviceInstance is null) - return null; - - if (type.GetMethod(MapCodeAsyncMethodName, [typeof(Document), typeof(ImmutableArray), typeof(ImmutableArray<(Document Document, TextSpan TextSpan)>), typeof(Dictionary), typeof(CancellationToken)]) is not MethodInfo mapCodeAsyncMethod) - return null; - - return (MapCodeAsyncDelegateType)Delegate.CreateDelegate(typeof(MapCodeAsyncDelegateType), serviceInstance, mapCodeAsyncMethod); - - } - catch - { - // Catch all here since failure is expected if user has no copilot chat or an older version of it installed. - } - - return null; - } - - public async Task?> MapCodeAsync(Document document, ImmutableArray contents, ImmutableArray<(Document document, TextSpan textSpan)> prioritizedFocusLocations, Dictionary options, CancellationToken cancellationToken) - { - if (_lazyMapCodeAsyncDelegate.Value is null) - return null; - - return await _lazyMapCodeAsyncDelegate.Value(document, contents, prioritizedFocusLocations, options, cancellationToken).ConfigureAwait(false); - } - } } diff --git a/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs b/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs index 1c361a9fc7003..71a70b2de65d9 100644 --- a/src/Tools/ExternalAccess/Razor/Cohost/Constants.cs +++ b/src/Tools/ExternalAccess/Razor/Cohost/Constants.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 Microsoft.CodeAnalysis.LanguageServer; @@ -16,4 +17,7 @@ internal static class Constants public const string RazorLanguageContract = ProtocolConstants.RazorCohostContract; public static readonly ImmutableArray RazorLanguage = ImmutableArray.Create("Razor"); + + // The UI context is provided by Razor, so this guid must match the one in https://github.com/dotnet/razor/blob/main/src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/RazorConstants.cs + public static readonly Guid RazorCohostingUIContext = new Guid("6d5b86dc-6b8a-483b-ae30-098a3c7d6774"); } diff --git a/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs b/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs index f3f84a2e888e5..425cfce0e740c 100644 --- a/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs +++ b/src/Tools/ExternalAccess/Razor/Cohost/RazorDynamicRegistrationServiceFactory.cs @@ -6,55 +6,84 @@ using System.Composition; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.LanguageServer; using Microsoft.CodeAnalysis.LanguageServer.Handler; using Newtonsoft.Json; using Roslyn.LanguageServer.Protocol; +using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.ExternalAccess.Razor.Cohost; [ExportCSharpVisualBasicLspServiceFactory(typeof(RazorDynamicRegistrationService), WellKnownLspServerKinds.AlwaysActiveVSLspServer), Shared] [method: ImportingConstructor] [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] -internal sealed class RazorDynamicRegistrationServiceFactory([Import(AllowDefault = true)] IRazorCohostDynamicRegistrationService? dynamicRegistrationService) : ILspServiceFactory +internal sealed class RazorDynamicRegistrationServiceFactory( + [Import(AllowDefault = true)] IUIContextActivationService? uIContextActivationService, + [Import(AllowDefault = true)] Lazy? dynamicRegistrationService) : ILspServiceFactory { public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind) { var clientLanguageServerManager = lspServices.GetRequiredService(); - return new RazorDynamicRegistrationService(dynamicRegistrationService, clientLanguageServerManager); + return new RazorDynamicRegistrationService(uIContextActivationService, dynamicRegistrationService, clientLanguageServerManager); } - private class RazorDynamicRegistrationService : ILspService, IOnInitialized + private class RazorDynamicRegistrationService( + IUIContextActivationService? uIContextActivationService, + Lazy? dynamicRegistrationService, + IClientLanguageServerManager? clientLanguageServerManager) : ILspService, IOnInitialized, IDisposable { - private readonly IRazorCohostDynamicRegistrationService? _dynamicRegistrationService; - private readonly IClientLanguageServerManager _clientLanguageServerManager; - private readonly JsonSerializerSettings _serializerSettings; + private readonly CancellationTokenSource _disposalTokenSource = new(); - public RazorDynamicRegistrationService(IRazorCohostDynamicRegistrationService? dynamicRegistrationService, IClientLanguageServerManager clientLanguageServerManager) + public void Dispose() { - _dynamicRegistrationService = dynamicRegistrationService; - _clientLanguageServerManager = clientLanguageServerManager; - - var serializer = new JsonSerializer(); - serializer.AddVSInternalExtensionConverters(); - _serializerSettings = new JsonSerializerSettings { Converters = serializer.Converters }; + _disposalTokenSource.Cancel(); } public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) { - if (_dynamicRegistrationService is null) + if (dynamicRegistrationService is null || clientLanguageServerManager is null) { return Task.CompletedTask; } + if (uIContextActivationService is null) + { + // Outside of VS, we want to initialize immediately.. I think? + InitializeRazor(); + } + else + { + uIContextActivationService.ExecuteWhenActivated(Constants.RazorCohostingUIContext, InitializeRazor); + } + + return Task.CompletedTask; + + void InitializeRazor() + { + this.InitializeRazor(clientCapabilities, context, _disposalTokenSource.Token); + } + } + + private void InitializeRazor(ClientCapabilities clientCapabilities, RequestContext context, CancellationToken cancellationToken) + { + // The LSP server will dispose us when the server exits, but VS could decide to activate us later. + // If a new instance of the server is created, a new instance of this class will be created and the + // UIContext will already be active, so this method will be immediately called on the new instance. + if (cancellationToken.IsCancellationRequested) return; + + var serializer = new JsonSerializer(); + serializer.AddVSInternalExtensionConverters(); + var serializerSettings = new JsonSerializerSettings { Converters = serializer.Converters }; + // We use a string to pass capabilities to/from Razor to avoid version issues with the Protocol DLL - var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities, _serializerSettings); - var razorCohostClientLanguageServerManager = new RazorCohostClientLanguageServerManager(_clientLanguageServerManager); + var serializedClientCapabilities = JsonConvert.SerializeObject(clientCapabilities, serializerSettings); + var razorCohostClientLanguageServerManager = new RazorCohostClientLanguageServerManager(clientLanguageServerManager!); var requestContext = new RazorCohostRequestContext(context); - return _dynamicRegistrationService.RegisterAsync(serializedClientCapabilities, requestContext, cancellationToken); + dynamicRegistrationService!.Value.RegisterAsync(serializedClientCapabilities, requestContext, cancellationToken).ReportNonFatalErrorAsync(); } } } diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/HotReloadRequestContext.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/HotReloadRequestContext.cs new file mode 100644 index 0000000000000..c7b511ee906c2 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/HotReloadRequestContext.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 Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using LSP = Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +internal sealed class HotReloadRequestContext(RequestContext context) +{ + internal LSP.ClientCapabilities ClientCapabilities => context.GetRequiredClientCapabilities(); + public TextDocument? TextDocument => context.TextDocument; + public Solution? Solution => context.Solution; + public bool IsTracking(TextDocument textDocument) => context.IsTracking(textDocument.GetURI()); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticManager.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticManager.cs new file mode 100644 index 0000000000000..f1fe7a600dae4 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticManager.cs @@ -0,0 +1,31 @@ +// 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.Collections.Generic; +using System.Collections.Immutable; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +internal interface IHotReloadDiagnosticManager +{ + /// + /// Refreshes hot reload diagnostics. + /// + void RequestRefresh(); + + /// + /// Registers providers of hot reload diagnostics. Callers are responsible for refreshing diagnostics after registration. + /// + void Register(IEnumerable providers); + + /// + /// Unregisters providers of hot reload diagnostics. Callers are responsible for refreshing diagnostics after un-registration. + /// + void Unregister(IEnumerable providers); + + /// + /// Providers. + /// + ImmutableArray Providers { get; } +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSource.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSource.cs new file mode 100644 index 0000000000000..85a8004f1db8d --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSource.cs @@ -0,0 +1,25 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +/// +/// Source of hot reload diagnostics. +/// +internal interface IHotReloadDiagnosticSource +{ + /// + /// Text document for which diagnostics are provided. + /// + DocumentId DocumentId { get; } + + /// + /// Provides list of diagnostics for the given document. + /// + ValueTask> GetDiagnosticsAsync(HotReloadRequestContext request, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSourceProvider.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..586a6a2d8ec90 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Contracts/IHotReloadDiagnosticSourceProvider.cs @@ -0,0 +1,27 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; + +/// +/// Provides diagnostic sources. +/// +internal interface IHotReloadDiagnosticSourceProvider +{ + /// + /// True if this provider is for documents. False if it is for a workspace, i.e. for unopened documents. + /// + bool IsDocument { get; } + + /// + /// Creates the diagnostic sources. + /// + /// The context. + /// The cancellation token. + ValueTask> CreateDiagnosticSourcesAsync(HotReloadRequestContext context, CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticManager.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticManager.cs new file mode 100644 index 0000000000000..8a6071ea4228a --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticManager.cs @@ -0,0 +1,49 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; +using Microsoft.CodeAnalysis.Host.Mef; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal; + +[Export(typeof(IHotReloadDiagnosticManager)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class HotReloadDiagnosticManager([Import] IDiagnosticsRefresher diagnosticsRefresher) : IHotReloadDiagnosticManager +{ + private readonly object _syncLock = new(); + public ImmutableArray Providers { get; private set; } = []; + + public void RequestRefresh() + => diagnosticsRefresher.RequestWorkspaceRefresh(); + + public void Register(IEnumerable providers) + { + // We use array instead of e.g. HashSet because we expect the number of sources to be small. + // Usually 2, one workspace and one document provider. + lock (_syncLock) + { + foreach (var provider in providers) + { + if (!Providers.Contains(provider)) + Providers = Providers.Add(provider); + } + } + } + + public void Unregister(IEnumerable providers) + { + lock (_syncLock) + { + foreach (var provider in Providers) + Providers = Providers.Remove(provider); + } + } + +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs new file mode 100644 index 0000000000000..64d157b4a1343 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSource.cs @@ -0,0 +1,32 @@ +// 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.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal; + +internal sealed class HotReloadDiagnosticSource(IHotReloadDiagnosticSource source, TextDocument textDocument) : IDiagnosticSource +{ + public async Task> GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) + { + var diagnostics = await source.GetDiagnosticsAsync(new HotReloadRequestContext(context), cancellationToken).ConfigureAwait(false); + var result = diagnostics.Select(diagnostic => DiagnosticData.Create(diagnostic, textDocument)).ToImmutableArray(); + return result; + } + + public TextDocumentIdentifier? GetDocumentIdentifier() => new() { Uri = textDocument.GetURI() }; + public ProjectOrDocumentId GetId() => new(textDocument.Id); + public Project GetProject() => textDocument.Project; + public bool IsLiveSource() => true; + public string ToDisplayString() => $"{this.GetType().Name}: {textDocument.FilePath ?? textDocument.Name} in {textDocument.Project.Name}"; +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSourceProvider.cs b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..495e586ea6f13 --- /dev/null +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Internal/HotReloadDiagnosticSourceProvider.cs @@ -0,0 +1,70 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal; + +internal abstract class HotReloadDiagnosticSourceProvider(IHotReloadDiagnosticManager diagnosticManager, bool isDocument) : IDiagnosticSourceProvider +{ + public string Name => "HotReloadDiagnostics"; + public bool IsDocument => isDocument; + + public async ValueTask> CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (context.Solution is not Solution solution) + { + return ImmutableArray.Empty; + } + + var hotReloadContext = new HotReloadRequestContext(context); + using var _ = ArrayBuilder.GetInstance(out var sources); + foreach (var provider in diagnosticManager.Providers) + { + if (provider.IsDocument == isDocument) + { + var hotReloadSources = await provider.CreateDiagnosticSourcesAsync(hotReloadContext, cancellationToken).ConfigureAwait(false); + foreach (var hotReloadSource in hotReloadSources) + { + // Look for additional document first. Currently most common hot reload diagnostics come from *.xaml files. + TextDocument? textDocument = + solution.GetAdditionalDocument(hotReloadSource.DocumentId) ?? + solution.GetDocument(hotReloadSource.DocumentId); + if (textDocument != null) + { + sources.Add(new HotReloadDiagnosticSource(hotReloadSource, textDocument)); + } + } + } + } + + var result = sources.ToImmutableAndClear(); + return DiagnosticSourceManager.AggregateSourcesIfNeeded(result, isDocument); + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class DocumentHotReloadDiagnosticSourceProvider([Import] IHotReloadDiagnosticManager diagnosticManager) + : HotReloadDiagnosticSourceProvider(diagnosticManager, isDocument: true) + { + } + + [Export(typeof(IDiagnosticSourceProvider)), Shared] + [method: ImportingConstructor] + [method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] + private sealed class WorkspaceHotReloadDiagnosticSourceProvider([Import] IHotReloadDiagnosticManager diagnosticManager) + : HotReloadDiagnosticSourceProvider(diagnosticManager, isDocument: false) + { + } +} diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt index 504106533fe6b..019251a6dad44 100644 --- a/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/VisualDiagnostics/InternalAPI.Unshipped.txt @@ -1,15 +1,46 @@ +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.ClientCapabilities.get -> Roslyn.LanguageServer.Protocol.ClientCapabilities! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.HotReloadRequestContext(Microsoft.CodeAnalysis.LanguageServer.Handler.RequestContext context) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.IsTracking(Microsoft.CodeAnalysis.TextDocument! textDocument) -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.Solution.get -> Microsoft.CodeAnalysis.Solution? +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext.TextDocument.get -> Microsoft.CodeAnalysis.TextDocument? +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.Providers.get -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.Register(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.RequestRefresh() -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager.Unregister(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource.DocumentId.get -> Microsoft.CodeAnalysis.DocumentId! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource.GetDiagnosticsAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext! request, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask> +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSourceProvider +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSourceProvider.CreateDiagnosticSourcesAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.HotReloadRequestContext! context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask> +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSourceProvider.IsDocument.get -> bool Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.HandleDiagnosticSessionStartAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.ProcessInfo info, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.HandleDiagnosticSessionStopAsync(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.ProcessInfo info, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.InitializeAsync(Microsoft.ServiceHub.Framework.IServiceBroker! serviceBroker, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsLanguageService.RequestDataBridgeConnectionAsync(string! connectionId, System.Threading.CancellationToken token) -> System.Threading.Tasks.Task! -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsServiceBroker -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IVisualDiagnosticsServiceBroker.GetServiceBrokerAsync() -> System.Threading.Tasks.Task! -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.NotifyServiceBrokerInitialized.get -> Microsoft.CodeAnalysis.BrokeredServices.IOnServiceBrokerInitialized? -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.NotifyServiceBrokerInitialized.set -> void -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.OnServiceBrokerInitialized(Microsoft.ServiceHub.Framework.IServiceBroker! serviceBroker) -> void -Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.VisualDiagnosticsServiceBroker.VisualDiagnosticsServiceBroker() -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.HotReloadDiagnosticManager(Microsoft.CodeAnalysis.Diagnostics.IDiagnosticsRefresher! diagnosticsRefresher) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.Providers.get -> System.Collections.Immutable.ImmutableArray +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.Register(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.RequestRefresh() -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticManager.Unregister(System.Collections.Generic.IEnumerable! providers) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetDiagnosticsAsync(Microsoft.CodeAnalysis.LanguageServer.Handler.RequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetDocumentIdentifier() -> Roslyn.LanguageServer.Protocol.TextDocumentIdentifier? +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetId() -> Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics.ProjectOrDocumentId +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.GetProject() -> Microsoft.CodeAnalysis.Project! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.HotReloadDiagnosticSource(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticSource! source, Microsoft.CodeAnalysis.TextDocument! textDocument) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.IsLiveSource() -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSource.ToDisplayString() -> string! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.CreateDiagnosticSourcesAsync(Microsoft.CodeAnalysis.LanguageServer.Handler.RequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask> +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.DocumentHotReloadDiagnosticSourceProvider.DocumentHotReloadDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager! diagnosticManager) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.HotReloadDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager! diagnosticManager, bool isDocument) -> void +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.IsDocument.get -> bool +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.Name.get -> string! +Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Internal.HotReloadDiagnosticSourceProvider.WorkspaceHotReloadDiagnosticSourceProvider.WorkspaceHotReloadDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.Contracts.IHotReloadDiagnosticManager! diagnosticManager) -> void Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.RunningProcessEntry Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.RunningProcessEntry.RunningProcessEntry() -> void Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.VisualDiagnosticsServiceFactory diff --git a/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj b/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj index ada2581f782b5..f63b387f65552 100644 --- a/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj +++ b/src/Tools/ExternalAccess/VisualDiagnostics/Microsoft.CodeAnalysis.ExternalAccess.VisualDiagnostics.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/Tools/ExternalAccess/Xaml/External/IXamlDiagnosticSource.cs b/src/Tools/ExternalAccess/Xaml/External/IXamlDiagnosticSource.cs new file mode 100644 index 0000000000000..0b55c2089007d --- /dev/null +++ b/src/Tools/ExternalAccess/Xaml/External/IXamlDiagnosticSource.cs @@ -0,0 +1,16 @@ +// 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.Collections.Immutable; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; + +internal interface IXamlDiagnosticSource +{ + Task> GetDiagnosticsAsync( + XamlRequestContext context, + CancellationToken cancellationToken); +} diff --git a/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs b/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs index e0f7cf9e77eda..812a79e593eef 100644 --- a/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs +++ b/src/Tools/ExternalAccess/Xaml/Internal/ClientCapabilityProvider.cs @@ -73,6 +73,12 @@ public bool IsDynamicRegistrationSupported(string methodName) return _clientCapabilities?.Workspace?.DidChangeConfiguration?.DynamicRegistration == true; case LSP.Methods.WorkspaceDidChangeWatchedFilesName: return _clientCapabilities?.Workspace?.DidChangeWatchedFiles?.DynamicRegistration == true; + case LSP.VSInternalMethods.OnAutoInsertName: + if (_clientCapabilities.TextDocument is VSInternalTextDocumentClientCapabilities internalTextDocumentClientCapabilities) + { + return internalTextDocumentClientCapabilities.OnAutoInsert?.DynamicRegistration == true; + } + return false; default: throw new InvalidOperationException($"Unsupported dynamic registration method: {methodName}"); } diff --git a/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs new file mode 100644 index 0000000000000..1cee9dbe1b173 --- /dev/null +++ b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSource.cs @@ -0,0 +1,32 @@ +// 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.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServer; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; +using Roslyn.LanguageServer.Protocol; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; + +internal sealed class XamlDiagnosticSource(IXamlDiagnosticSource xamlDiagnosticSource, TextDocument document) : IDiagnosticSource +{ + bool IDiagnosticSource.IsLiveSource() => true; + Project IDiagnosticSource.GetProject() => document.Project; + ProjectOrDocumentId IDiagnosticSource.GetId() => new(document.Id); + TextDocumentIdentifier? IDiagnosticSource.GetDocumentIdentifier() => new() { Uri = document.GetURI() }; + string IDiagnosticSource.ToDisplayString() => $"{this.GetType().Name}: {document.FilePath ?? document.Name} in {document.Project.Name}"; + + async Task> IDiagnosticSource.GetDiagnosticsAsync(RequestContext context, CancellationToken cancellationToken) + { + var xamlRequestContext = XamlRequestContext.FromRequestContext(context); + var diagnostics = await xamlDiagnosticSource.GetDiagnosticsAsync(xamlRequestContext, cancellationToken).ConfigureAwait(false); + var result = diagnostics.Select(e => DiagnosticData.Create(e, document)).ToImmutableArray(); + return result; + } +} diff --git a/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSourceProvider.cs b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSourceProvider.cs new file mode 100644 index 0000000000000..110413bfdb9ef --- /dev/null +++ b/src/Tools/ExternalAccess/Xaml/Internal/XamlDiagnosticSourceProvider.cs @@ -0,0 +1,35 @@ +// 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 System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.LanguageServer.Handler; +using Microsoft.CodeAnalysis.LanguageServer.Handler.Diagnostics; + +namespace Microsoft.CodeAnalysis.ExternalAccess.Xaml; + +[Export(typeof(IDiagnosticSourceProvider)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class XamlDiagnosticSourceProvider([Import(AllowDefault = true)] IXamlDiagnosticSource? xamlDiagnosticSource) : IDiagnosticSourceProvider +{ + bool IDiagnosticSourceProvider.IsDocument => true; + + string IDiagnosticSourceProvider.Name => "XamlDiagnostics"; + + ValueTask> IDiagnosticSourceProvider.CreateDiagnosticSourcesAsync(RequestContext context, CancellationToken cancellationToken) + { + if (xamlDiagnosticSource != null && context.TextDocument is { } document && + document.Project.GetAdditionalDocument(document.Id) != null) + { + return new([new XamlDiagnosticSource(xamlDiagnosticSource, document)]); + } + + return new([]); + } +} diff --git a/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt b/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt index d62fe5bb29af1..c8d63468defae 100644 --- a/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt +++ b/src/Tools/ExternalAccess/Xaml/InternalAPI.Unshipped.txt @@ -37,6 +37,8 @@ Microsoft.CodeAnalysis.ExternalAccess.Xaml.ILocationService.GetSymbolLocationsAs Microsoft.CodeAnalysis.ExternalAccess.Xaml.IResolveCachedDataService Microsoft.CodeAnalysis.ExternalAccess.Xaml.IResolveCachedDataService.FromResolveData(object? resolveData) -> (object? data, System.Uri? uri) Microsoft.CodeAnalysis.ExternalAccess.Xaml.IResolveCachedDataService.ToResolveData(object! data, System.Uri! uri) -> object! +Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource.GetDiagnosticsAsync(Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlRequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task>! Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlRequestHandler Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlRequestHandler.HandleRequestAsync(TRequest request, Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlRequestContext context, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlRequestHandler.MutatesSolutionState.get -> bool @@ -51,6 +53,10 @@ Microsoft.CodeAnalysis.ExternalAccess.Xaml.ResolveDataConversions Microsoft.CodeAnalysis.ExternalAccess.Xaml.StringConstants Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlCommandAttribute Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlCommandAttribute.XamlCommandAttribute(string! command) -> void +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSource +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSource.XamlDiagnosticSource(Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource! xamlDiagnosticSource, Microsoft.CodeAnalysis.TextDocument! document) -> void +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSourceProvider +Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlDiagnosticSourceProvider.XamlDiagnosticSourceProvider(Microsoft.CodeAnalysis.ExternalAccess.Xaml.IXamlDiagnosticSource? xamlDiagnosticSource) -> void Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlMethodAttribute Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlMethodAttribute.XamlMethodAttribute(string! method) -> void Microsoft.CodeAnalysis.ExternalAccess.Xaml.XamlRequestContext diff --git a/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs b/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs index 450ff38f04e2f..1453ad36fe7dc 100644 --- a/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs +++ b/src/VisualStudio/CSharp/Impl/ChangeSignature/CSharpChangeSignatureViewModelFactoryService.cs @@ -47,7 +47,7 @@ public override SymbolDisplayPart[] GeneratePreviewDisplayParts(AddedParameterVi parts.Add(new SymbolDisplayPart(SymbolDisplayPartKind.Text, null, addedParameterViewModel.Default)); } - return parts.ToArray(); + return [.. parts]; } // Use LangVersion Preview to ensure that all types parse correctly. If the user types in a type only available diff --git a/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs b/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs index 6544889756b73..c5481121e2963 100644 --- a/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs +++ b/src/VisualStudio/Core/Def/EditorConfigSettings/SettingsEditorControl.xaml.cs @@ -66,7 +66,7 @@ public SettingsEditorControl(ISettingsEditorView whitespaceView, analyzerView ]; - _tableControls = _views.SelectAsArray(view => view.TableControl).ToArray(); + _tableControls = [.. _views.SelectAsArray(view => view.TableControl)]; InitializeComponent(); } diff --git a/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs b/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs index 9e5a817ba411b..49794d90e20f2 100644 --- a/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs +++ b/src/VisualStudio/Core/Def/ErrorReporting/VisualStudioErrorReportingService.cs @@ -75,7 +75,7 @@ public void ShowFeatureNotAvailableErrorInfo(string message, TelemetryFeatureNam closeAfterAction: true)); } - ShowGlobalErrorInfo(message, featureName, exception, infoBarUIs.ToArray()); + ShowGlobalErrorInfo(message, featureName, exception, [.. infoBarUIs]); } private void LogGlobalErrorToActivityLog(string message, string? detailedError) diff --git a/src/VisualStudio/Core/Def/Implementation/VisualStudioUIContextActivationService.cs b/src/VisualStudio/Core/Def/Implementation/VisualStudioUIContextActivationService.cs new file mode 100644 index 0000000000000..d04d43a5cd83c --- /dev/null +++ b/src/VisualStudio/Core/Def/Implementation/VisualStudioUIContextActivationService.cs @@ -0,0 +1,23 @@ +// 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.Composition; +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.LanguageServices.Implementation; + +[Export(typeof(IUIContextActivationService)), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VisualStudioUIContextActivationService() : IUIContextActivationService +{ + public void ExecuteWhenActivated(Guid uiContext, Action action) + { + var context = UIContext.FromUIContextGuid(uiContext); + context.WhenActivated(action); + } +} diff --git a/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs b/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs index 2cc9eb4b5c06b..a2a015311f028 100644 --- a/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs +++ b/src/VisualStudio/Core/Def/Implementation/VsRefactorNotifyService.cs @@ -46,7 +46,7 @@ public bool TryOnBeforeGlobalSymbolRenamed(Workspace workspace, IEnumerable GetPartialForNamedTypeAsync(INamedTypeSym partials.Add(GraphNodeId.GetArray( CodeGraphNodeIdName.GenericArgumentsIdentifier, - genericArguments.ToArray())); + [.. genericArguments])); } if (namedType.ContainingType != null) @@ -206,7 +206,7 @@ private static async Task GetPartialForNamedTypeAsync(INamedTypeSym partials.Add(await GetPartialForTypeAsync(namedType.ContainingType, CodeGraphNodeIdName.ParentType, solution, cancellationToken, hasGenericArguments).ConfigureAwait(false)); } - return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary(partials.ToArray())); + return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary([.. partials])); } } @@ -231,7 +231,7 @@ private static async Task GetPartialForPointerTypeAsync(IPointerTyp partials.Add(await GetPartialForTypeAsync(pointerType.PointedAtType.ContainingType, CodeGraphNodeIdName.ParentType, solution, cancellationToken).ConfigureAwait(false)); } - return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary(partials.ToArray())); + return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary([.. partials])); } private static async Task GetPartialForArrayTypeAsync(IArrayTypeSymbol arrayType, GraphNodeIdName nodeName, Solution solution, CancellationToken cancellationToken) @@ -252,7 +252,7 @@ private static async Task GetPartialForArrayTypeAsync(IArrayTypeSym partials.Add(GraphNodeId.GetPartial(CodeQualifiedName.ArrayRank, arrayType.Rank.ToString())); partials.Add(await GetPartialForTypeAsync(arrayType.ElementType, CodeGraphNodeIdName.ParentType, solution, cancellationToken).ConfigureAwait(false)); - return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary(partials.ToArray())); + return GraphNodeId.GetPartial(nodeName, MakeCollectionIfNecessary([.. partials])); } private static async Task GetPartialForTypeParameterSymbolAsync(ITypeParameterSymbol typeParameterSymbol, GraphNodeIdName nodeName, Solution solution, CancellationToken cancellationToken) @@ -317,7 +317,7 @@ public static async Task GetIdForMemberAsync(ISymbol member, Soluti nodes.Add(GraphNodeId.GetPartial(CodeGraphNodeIdName.ParamKind, ParamKind.Ref)); } - parameterTypeIds.Add(GraphNodeId.GetNested(nodes.ToArray())); + parameterTypeIds.Add(GraphNodeId.GetNested([.. nodes])); } if (member is IMethodSymbol methodSymbol && methodSymbol.MethodKind == MethodKind.Conversion) @@ -336,25 +336,25 @@ public static async Task GetIdForMemberAsync(ISymbol member, Soluti var returnTypePartial = nodes.ToList(); returnTypePartial.Add(GraphNodeId.GetPartial(CodeGraphNodeIdName.ParamKind, Microsoft.VisualStudio.GraphModel.CodeSchema.ParamKind.Return)); - var returnCollection = GraphNodeId.GetNested(returnTypePartial.ToArray()); + var returnCollection = GraphNodeId.GetNested([.. returnTypePartial]); parameterTypeIds.Add(returnCollection); } memberPartials.Add(GraphNodeId.GetArray( CodeGraphNodeIdName.OverloadingParameters, - parameterTypeIds.ToArray())); + [.. parameterTypeIds])); } partials.Add(GraphNodeId.GetPartial( CodeGraphNodeIdName.Member, - MakeCollectionIfNecessary(memberPartials.ToArray()))); + MakeCollectionIfNecessary([.. memberPartials]))); } else { partials.Add(GraphNodeId.GetPartial(CodeGraphNodeIdName.Member, member.MetadataName)); } - return GraphNodeId.GetNested(partials.ToArray()); + return GraphNodeId.GetNested([.. partials]); } private static object MakeCollectionIfNecessary(GraphNodeId[] array) diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs index 8b8d1b5f0cc5f..761a41ea3d178 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReference.Snapshot.cs @@ -110,7 +110,7 @@ protected override PortableExecutableReference WithPropertiesImpl(MetadataRefere private string GetDebuggerDisplay() => "Metadata File: " + FilePath; - public IReadOnlyList GetStorages() - => _provider.GetStorages(this.FilePath, _timestamp.Value); + public IReadOnlyList StorageHandles + => _provider.GetStorageHandles(this.FilePath, _timestamp.Value); } } diff --git a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs index 8fe74c01b94e9..01a6a41f6695d 100644 --- a/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs +++ b/src/VisualStudio/Core/Def/ProjectSystem/MetadataReferences/VisualStudioMetadataReferenceManager.cs @@ -22,6 +22,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; +using static TemporaryStorageService; + /// /// Manages metadata references for VS projects. /// @@ -37,13 +39,13 @@ internal sealed partial class VisualStudioMetadataReferenceManager : IWorkspaceS private static readonly ConditionalWeakTable s_lifetimeMap = new(); /// - /// Mapping from an we created, to the memory mapped files (mmf) corresponding to - /// the assembly and all the modules within it. This is kept around to make OOP syncing more efficient. - /// Specifically, since we know we read the assembly into an mmf, we can just send the mmf name/offset/length to - /// the remote process, and it can map that same memory in directly, instead of needing the host to send the - /// entire contents of the assembly over the channel to the OOP process. + /// Mapping from an we created, to the identifiers identifying the memory mapped + /// files (mmf) corresponding to that assembly and all the modules within it. This is kept around to make OOP + /// syncing more efficient. Specifically, since we know we dumped the assembly into an mmf, we can just send the mmf + /// name/offset/length to the remote process, and it can map that same memory in directly, instead of needing the + /// host to send the entire contents of the assembly over the channel to the OOP process. /// - private static readonly ConditionalWeakTable> s_metadataToStorages = new(); + private static readonly ConditionalWeakTable> s_metadataToStorageHandles = new(); private readonly MetadataCache _metadataCache = new(); private readonly ImmutableArray _runtimeDirectories; @@ -86,14 +88,14 @@ public void Dispose() } } - public IReadOnlyList? GetStorages(string fullPath, DateTime snapshotTimestamp) + public IReadOnlyList? GetStorageHandles(string fullPath, DateTime snapshotTimestamp) { var key = new FileKey(fullPath, snapshotTimestamp); // check existing metadata if (_metadataCache.TryGetMetadata(key, out var source) && - s_metadataToStorages.TryGetValue(source, out var storages)) + s_metadataToStorageHandles.TryGetValue(source, out var handles)) { - return storages; + return handles; } return null; @@ -154,45 +156,35 @@ AssemblyMetadata GetMetadataWorker() else { // use temporary storage - using var _ = ArrayBuilder.GetInstance(out var storages); - var newMetadata = CreateAssemblyMetadata(key, key => - { - // - // - GetMetadataFromTemporaryStorage(key, out var storage, out var metadata); - storages.Add(storage); - return metadata; - }); - - var storagesArray = storages.ToImmutable(); - - s_metadataToStorages.Add(newMetadata, storagesArray); - - return newMetadata; + return CreateAssemblyMetadata(key, GetMetadataFromTemporaryStorage); } } } - private void GetMetadataFromTemporaryStorage( - FileKey moduleFileKey, out TemporaryStorageService.TemporaryStreamStorage storage, out ModuleMetadata metadata) + private (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) GetMetadataFromTemporaryStorage( + FileKey moduleFileKey) { - GetStorageInfoFromTemporaryStorage(moduleFileKey, out storage, out var stream); + GetStorageInfoFromTemporaryStorage(moduleFileKey, out var storageHandle, out var stream); unsafe { - // For an unmanaged memory stream, ModuleMetadata can take ownership directly. - metadata = ModuleMetadata.CreateFromMetadata((IntPtr)stream.PositionPointer, (int)stream.Length, stream.Dispose); + // For an unmanaged memory stream, ModuleMetadata can take ownership directly. Passing in stream.Dispose + // here will also ensure that as long as this metdata is alive, we'll keep the memory-mapped-file it points + // to alive. + var metadata = ModuleMetadata.CreateFromMetadata((IntPtr)stream.PositionPointer, (int)stream.Length, stream.Dispose); + return (metadata, storageHandle); } - return; - void GetStorageInfoFromTemporaryStorage( - FileKey moduleFileKey, out TemporaryStorageService.TemporaryStreamStorage storage, out UnmanagedMemoryStream stream) + FileKey moduleFileKey, out TemporaryStorageStreamHandle storageHandle, out UnmanagedMemoryStream stream) { int size; + + // Create a temp stream in memory to copy the metadata bytes into. using (var copyStream = SerializableBytes.CreateWritableStream()) { - // open a file and let it go as soon as possible + // Open a file on disk, find the metadata section, copy those bytes into the temp stream, and release + // the file immediately after. using (var fileStream = FileUtilities.OpenRead(moduleFileKey.FullPath)) { var headers = new PEHeaders(fileStream); @@ -210,16 +202,15 @@ void GetStorageInfoFromTemporaryStorage( StreamCopy(fileStream, copyStream, offset, size); } - // copy over the data to temp storage and let pooled stream go - storage = _temporaryStorageService.CreateTemporaryStreamStorage(); - + // Now, copy over the metadata bytes into a memory mapped file. This will keep it fixed in a single + // location, so we can create a metadata value wrapping that. This will also let us share the memory + // for that metadata value with our OOP process. copyStream.Position = 0; - storage.WriteStream(copyStream); + storageHandle = _temporaryStorageService.WriteToTemporaryStorage(copyStream, CancellationToken.None); } - // get stream that owns the underlying unmanaged memory. - stream = storage.ReadStream(CancellationToken.None); - + // Now, read the data from the memory-mapped-file back into a stream that we load into the metadata value. + stream = storageHandle.ReadFromTemporaryStorage(CancellationToken.None); // stream size must be same as what metadata reader said the size should be. Contract.ThrowIfFalse(stream.Length == size); } @@ -249,12 +240,11 @@ private AssemblyMetadata CreateAssemblyMetadataFromMetadataImporter(FileKey file return CreateAssemblyMetadata(fileKey, fileKey => { var metadata = TryCreateModuleMetadataFromMetadataImporter(fileKey); + if (metadata != null) + return (metadata, storageHandle: null); // getting metadata didn't work out through importer. fallback to shadow copy one - if (metadata == null) - GetMetadataFromTemporaryStorage(fileKey, out _, out metadata); - - return metadata; + return GetMetadataFromTemporaryStorage(fileKey); }); ModuleMetadata? TryCreateModuleMetadataFromMetadataImporter(FileKey moduleFileKey) @@ -311,11 +301,12 @@ bool TryGetFileMappingFromMetadataImporter(FileKey fileKey, [NotNullWhen(true)] /// private static AssemblyMetadata CreateAssemblyMetadata( FileKey fileKey, - Func moduleMetadataFactory) + Func moduleMetadataFactory) { - var manifestModule = moduleMetadataFactory(fileKey); + var (manifestModule, manifestHandle) = moduleMetadataFactory(fileKey); - using var _ = ArrayBuilder.GetInstance(out var moduleBuilder); + using var _1 = ArrayBuilder.GetInstance(out var moduleBuilder); + using var _2 = ArrayBuilder.GetInstance(out var storageHandles); string? assemblyDir = null; foreach (var moduleName in manifestModule.GetModuleNames()) @@ -328,14 +319,27 @@ private static AssemblyMetadata CreateAssemblyMetadata( // Suppression should be removed or addressed https://github.com/dotnet/roslyn/issues/41636 var moduleFileKey = FileKey.Create(PathUtilities.CombineAbsoluteAndRelativePaths(assemblyDir, moduleName)!); - var metadata = moduleMetadataFactory(moduleFileKey); + var (metadata, metadataStorageHandle) = moduleMetadataFactory(moduleFileKey); moduleBuilder.Add(metadata); + storageHandles.Add(metadataStorageHandle); } if (moduleBuilder.Count == 0) + { moduleBuilder.Add(manifestModule); + storageHandles.Add(manifestHandle); + } + + var result = AssemblyMetadata.Create(moduleBuilder.ToImmutable()); + + // If we got any null handles, then we weren't able to map this whole assembly into memory mapped files. So we + // can't use those to transfer over the data efficiently to the OOP process. In that case, we don't store the + // handles at all. + Contract.ThrowIfTrue(storageHandles.Count == 0); + if (storageHandles.All(h => h != null)) + s_metadataToStorageHandles.Add(result, storageHandles.ToImmutable()); - return AssemblyMetadata.Create(moduleBuilder.ToImmutable()); + return result; } } diff --git a/src/VisualStudio/Core/Def/RoslynPackage.cs b/src/VisualStudio/Core/Def/RoslynPackage.cs index 79a33a370c61e..93730649e413e 100644 --- a/src/VisualStudio/Core/Def/RoslynPackage.cs +++ b/src/VisualStudio/Core/Def/RoslynPackage.cs @@ -273,7 +273,7 @@ private async Task LoadComponentsBackgroundAsync(CancellationToken cancellationT await LoadStackTraceExplorerMenusAsync(cancellationToken).ConfigureAwait(true); // Initialize keybinding reset detector - await ComponentModel.DefaultExportProvider.GetExportedValue().InitializeAsync().ConfigureAwait(true); + await ComponentModel.DefaultExportProvider.GetExportedValue().InitializeAsync(cancellationToken).ConfigureAwait(true); } private async Task LoadStackTraceExplorerMenusAsync(CancellationToken cancellationToken) diff --git a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/AbstractSnippetFunction.cs b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/AbstractSnippetFunction.cs index 61aba67eadf6e..d7145bc6db4ea 100644 --- a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/AbstractSnippetFunction.cs +++ b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/AbstractSnippetFunction.cs @@ -27,18 +27,13 @@ public AbstractSnippetFunction(SnippetExpansionClient snippetExpansionClient, IT _threadingContext = threadingContext; } - protected bool TryGetDocument([NotNullWhen(true)] out Document? document) - { - document = _subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges()?.WithFrozenPartialSemantics(CancellationToken.None); - return document != null; - } + protected Document? GetDocument(CancellationToken cancellationToken) + => _subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges()?.WithFrozenPartialSemantics(cancellationToken); private int GetDefaultValue(CancellationToken cancellationToken, out string value, out int hasDefaultValue) { - var (ExitCode, Value, HasDefaultValue) = _threadingContext.JoinableTaskFactory.Run(() => GetDefaultValueAsync(cancellationToken)); - value = Value; - hasDefaultValue = HasDefaultValue; - return ExitCode; + (var exitCode, value, hasDefaultValue) = _threadingContext.JoinableTaskFactory.Run(() => GetDefaultValueAsync(cancellationToken)); + return exitCode; } protected virtual Task<(int ExitCode, string Value, int HasDefaultValue)> GetDefaultValueAsync(CancellationToken cancellationToken) @@ -48,10 +43,8 @@ private int GetDefaultValue(CancellationToken cancellationToken, out string valu private int GetCurrentValue(CancellationToken cancellationToken, out string value, out int hasCurrentValue) { - var (ExitCode, Value, HasCurrentValue) = _threadingContext.JoinableTaskFactory.Run(() => GetCurrentValueAsync(cancellationToken)); - value = Value; - hasCurrentValue = HasCurrentValue; - return ExitCode; + (var exitCode, value, hasCurrentValue) = _threadingContext.JoinableTaskFactory.Run(() => GetCurrentValueAsync(cancellationToken)); + return exitCode; } protected virtual Task<(int ExitCode, string Value, int HasCurrentValue)> GetCurrentValueAsync(CancellationToken cancellationToken) diff --git a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionClassName.cs b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionClassName.cs index a732df3a371ef..015dd6d99bc9e 100644 --- a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionClassName.cs +++ b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionClassName.cs @@ -28,29 +28,22 @@ public SnippetFunctionClassName(SnippetExpansionClient snippetExpansionClient, I { var hasDefaultValue = 0; var value = string.Empty; - if (!TryGetDocument(out var document)) - { + var document = GetDocument(cancellationToken); + if (document is null) return (VSConstants.E_FAIL, value, hasDefaultValue); - } var surfaceBufferFieldSpan = new VsTextSpan[1]; if (snippetExpansionClient.ExpansionSession.GetFieldSpan(FieldName, surfaceBufferFieldSpan) != VSConstants.S_OK) - { return (VSConstants.E_FAIL, value, hasDefaultValue); - } if (!snippetExpansionClient.TryGetSubjectBufferSpan(surfaceBufferFieldSpan[0], out var subjectBufferFieldSpan)) - { return (VSConstants.E_FAIL, value, hasDefaultValue); - } var snippetFunctionService = document.Project.GetRequiredLanguageService(); value = await snippetFunctionService.GetContainingClassNameAsync(document, subjectBufferFieldSpan.Start.Position, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(value)) - { hasDefaultValue = 1; - } return (VSConstants.S_OK, value, hasDefaultValue); } diff --git a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionGenerateSwitchCases.cs b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionGenerateSwitchCases.cs index 6044dce6fe387..526e0a7091825 100644 --- a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionGenerateSwitchCases.cs +++ b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionGenerateSwitchCases.cs @@ -41,10 +41,9 @@ protected override int FieldChanged(string field, out int requeryFunction) protected override async Task<(int ExitCode, string Value, int HasCurrentValue)> GetCurrentValueAsync(CancellationToken cancellationToken) { - if (!TryGetDocument(out var document)) - { + var document = GetDocument(cancellationToken); + if (document is null) return (VSConstants.S_OK, string.Empty, HasCurrentValue: 0); - } // If the switch expression is invalid, still show the default case var hasCurrentValue = 1; @@ -60,9 +59,7 @@ protected override int FieldChanged(string field, out int requeryFunction) var value = await snippetFunctionService.GetSwitchExpansionAsync(document, caseGenerationSpan.Value, switchExpressionSpan.Value, simplifierOptions, cancellationToken).ConfigureAwait(false); if (value == null) - { return (VSConstants.S_OK, snippetFunctionService.SwitchDefaultCaseForm, hasCurrentValue); - } return (VSConstants.S_OK, value, hasCurrentValue); } diff --git a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionSimpleTypeName.cs b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionSimpleTypeName.cs index 81ce994883a1e..77665d5efd398 100644 --- a/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionSimpleTypeName.cs +++ b/src/VisualStudio/Core/Def/Snippets/SnippetFunctions/SnippetFunctionSimpleTypeName.cs @@ -35,23 +35,18 @@ public SnippetFunctionSimpleTypeName( { var value = _fullyQualifiedName; var hasDefaultValue = 1; - if (!TryGetDocument(out var document)) - { + var document = GetDocument(cancellationToken); + if (document is null) return (VSConstants.E_FAIL, value, hasDefaultValue); - } if (!TryGetFieldSpan(out var fieldSpan)) - { return (VSConstants.E_FAIL, value, hasDefaultValue); - } var simplifierOptions = await document.GetSimplifierOptionsAsync(snippetExpansionClient.EditorOptionsService.GlobalOptions, cancellationToken).ConfigureAwait(false); var simplifiedTypeName = await SnippetFunctionService.GetSimplifiedTypeNameAsync(document, fieldSpan.Value, _fullyQualifiedName, simplifierOptions, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(simplifiedTypeName)) - { return (VSConstants.E_FAIL, value, hasDefaultValue); - } return (VSConstants.S_OK, simplifiedTypeName!, hasDefaultValue); } diff --git a/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs b/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs index ace4d7cfc5ea6..e15e7d56ecbc2 100644 --- a/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs +++ b/src/VisualStudio/Core/Def/UnusedReferences/Dialog/UnusedReferencesTableProvider.cs @@ -40,7 +40,7 @@ public IWpfTableControl4 CreateTableControl() _tableManager, autoSubscribe: true, BuildColumnStates(), - UnusedReferencesColumnDefinitions.ColumnNames.ToArray()); + [.. UnusedReferencesColumnDefinitions.ColumnNames]); tableControl.ShowGroupingLine = true; tableControl.DoColumnsAutoAdjust = true; tableControl.DoSortingAndGroupingWhileUnstable = true; diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioActiveDocumentTracker.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioActiveDocumentTracker.cs index 828b7bd7e15d3..73ead9d0ebedc 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioActiveDocumentTracker.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioActiveDocumentTracker.cs @@ -7,10 +7,7 @@ using System.ComponentModel.Composition; using System.Linq; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.Editor; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.Editor.Shared.Utilities; using Microsoft.CodeAnalysis.Host.Mef; diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentTrackingServiceFactory.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentTrackingServiceFactory.cs index 8a03e1c78d83f..2273efa36396d 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentTrackingServiceFactory.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioDocumentTrackingServiceFactory.cs @@ -12,35 +12,21 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation; [ExportWorkspaceServiceFactory(typeof(IDocumentTrackingService), ServiceLayer.Host), Shared] -internal sealed class VisualStudioDocumentTrackingServiceFactory : IWorkspaceServiceFactory +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class VisualStudioDocumentTrackingServiceFactory(VisualStudioActiveDocumentTracker activeDocumentTracker) : IWorkspaceServiceFactory { - private readonly VisualStudioActiveDocumentTracker _activeDocumentTracker; - - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public VisualStudioDocumentTrackingServiceFactory(VisualStudioActiveDocumentTracker activeDocumentTracker) - => _activeDocumentTracker = activeDocumentTracker; - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => new VisualStudioDocumentTrackingService(_activeDocumentTracker, workspaceServices.Workspace); + => new VisualStudioDocumentTrackingService(activeDocumentTracker, workspaceServices.Workspace); - private class VisualStudioDocumentTrackingService : IDocumentTrackingService + private class VisualStudioDocumentTrackingService(VisualStudioActiveDocumentTracker activeDocumentTracker, Workspace workspace) : IDocumentTrackingService { - private readonly VisualStudioActiveDocumentTracker _activeDocumentTracker; - private readonly Workspace _workspace; - - public VisualStudioDocumentTrackingService(VisualStudioActiveDocumentTracker activeDocumentTracker, Workspace workspace) - { - _activeDocumentTracker = activeDocumentTracker; - _workspace = workspace; - } - + private readonly VisualStudioActiveDocumentTracker _activeDocumentTracker = activeDocumentTracker; + private readonly Workspace _workspace = workspace; private readonly object _gate = new(); private int _subscriptions = 0; private EventHandler? _activeDocumentChangedEventHandler; - public bool SupportsDocumentTracking => true; - public event EventHandler ActiveDocumentChanged { add diff --git a/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs b/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs index c703b64497112..8013cdac4f854 100644 --- a/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs +++ b/src/VisualStudio/Core/Def/Workspace/VisualStudioSymbolNavigationService.cs @@ -180,7 +180,6 @@ await navigationService.TryNavigateToSpanAsync( public async Task TrySymbolNavigationNotifyAsync(ISymbol symbol, Project project, CancellationToken cancellationToken) { await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); - _threadingContext.ThrowIfNotOnUIThread(); var definitionItem = symbol.ToNonClassifiedDefinitionItem(project.Solution, includeHiddenLocations: true); definitionItem.Properties.TryGetValue(DefinitionItem.RQNameKey1, out var rqName); diff --git a/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs b/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs index 3a1213d0d2013..4a980f4e05115 100644 --- a/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs +++ b/src/VisualStudio/Core/Impl/CodeModel/FileCodeModel_CodeGen.cs @@ -141,7 +141,7 @@ private static object[] GetValidArray(object itemOrArray, bool allowMultipleElem } } - return result.ToArray(); + return [.. result]; } internal EnvDTE80.CodeAttributeArgument AddAttributeArgument(SyntaxNode containerNode, string name, string value, object position) diff --git a/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs b/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs index 75d7d1abb1dd7..3c1acd9e7c9aa 100644 --- a/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs +++ b/src/VisualStudio/Core/Impl/CodeModel/ProjectCodeModelFactory.cs @@ -110,7 +110,7 @@ private async ValueTask ProcessNextDocumentBatchAsync( // Keep firing events for this doc, as long as we haven't exceeded the max amount // of waiting time, and there's no user input that should take precedence. - if (stopwatch.Elapsed.Ticks > MaxTimeSlice || IThreadingContextExtensions.IsInputPending()) + if (stopwatch.Elapsed.Ticks > MaxTimeSlice || IsInputPending()) { await this.Listener.Delay(delayBetweenProcessing, cancellationToken).ConfigureAwait(true); stopwatch = SharedStopwatch.StartNew(); @@ -140,6 +140,21 @@ void FireEventsForDocument(DocumentId documentId) codeModel.FireEvents(); return; } + + // Returns true if any keyboard or mouse button input is pending on the message queue. + static bool IsInputPending() + { + // 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; + } } private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) diff --git a/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs b/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs index 9ca3dba4d904b..94ff8208e4117 100644 --- a/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs +++ b/src/VisualStudio/Core/Impl/Options/AbstractOptionPreviewViewModel.cs @@ -148,7 +148,7 @@ public void UpdatePreview(string text) _editorOptions.CreateOptions(), textBuffer.CurrentSnapshot, separator: "", - exposedLineSpans: GetExposedLineSpans(textBuffer.CurrentSnapshot).ToArray()); + exposedLineSpans: [.. GetExposedLineSpans(textBuffer.CurrentSnapshot)]); var textView = _textEditorFactoryService.CreateTextView(projection, _textEditorFactoryService.CreateTextViewRoleSet(PredefinedTextViewRoles.Interactive)); @@ -224,7 +224,7 @@ protected void AddParenthesesOption( isChecked: !defaultAddForClarity)); CodeStyleItems.Add(new EnumCodeStyleOptionViewModel( - languageOption, title, preferences.ToArray(), + languageOption, title, [.. preferences], examples, this, optionStore, ServicesVSResources.Parentheses_preferences_colon, codeStylePreferences)); } diff --git a/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs b/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs index 983d1474bfea5..75f2401bcaaf0 100644 --- a/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs +++ b/src/VisualStudio/Core/Impl/Options/Style/NamingPreferences/NamingStyleOptionPageControl.xaml.cs @@ -72,7 +72,7 @@ private void AddButton_Click(object sender, RoutedEventArgs e) private void ManageSpecificationsButton_Click(object sender, RoutedEventArgs e) { - var viewModel = new ManageSymbolSpecificationsDialogViewModel(_viewModel.Specifications, _viewModel.CodeStyleItems.ToList(), _languageName, _notificationService); + var viewModel = new ManageSymbolSpecificationsDialogViewModel(_viewModel.Specifications, [.. _viewModel.CodeStyleItems], _languageName, _notificationService); var dialog = new ManageNamingStylesInfoDialog(viewModel); if (dialog.ShowModal().Value == true) { @@ -82,7 +82,7 @@ private void ManageSpecificationsButton_Click(object sender, RoutedEventArgs e) private void ManageStylesButton_Click(object sender, RoutedEventArgs e) { - var viewModel = new ManageNamingStylesDialogViewModel(_viewModel.NamingStyles, _viewModel.CodeStyleItems.ToList(), _notificationService); + var viewModel = new ManageNamingStylesDialogViewModel(_viewModel.NamingStyles, [.. _viewModel.CodeStyleItems], _notificationService); var dialog = new ManageNamingStylesInfoDialog(viewModel); if (dialog.ShowModal().Value == true) { diff --git a/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs b/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs index 77cd8df2ccb65..632755a0b1a82 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SerializationValidator.cs @@ -94,11 +94,10 @@ public async Task GetValueAsync(Checksum checksum) var data = await GetRequiredAssetAsync(checksum).ConfigureAwait(false); Contract.ThrowIfNull(data.Value); - using var context = new SolutionReplicationContext(); using var stream = SerializableBytes.CreateWritableStream(); using (var writer = new ObjectWriter(stream, leaveOpen: true)) { - Serializer.Serialize(data.Value, writer, context, CancellationToken.None); + Serializer.Serialize(data.Value, writer, CancellationToken.None); } stream.Position = 0; @@ -189,7 +188,7 @@ internal async Task VerifyAssetAsync(Checksum attributeChecksum, Checksum textCh await VerifyAssetSerializationAsync( textChecksum, WellKnownSynchronizationKind.SerializableSourceText, - (v, k, s) => new SolutionAsset(s.CreateChecksum(v, CancellationToken.None), v)); + (v, k, s) => new SolutionAsset(v.ContentChecksum, v)); } internal async Task VerifyAssetSerializationAsync( diff --git a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs index bbb5f577a5433..d638391df2977 100644 --- a/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs +++ b/src/VisualStudio/Core/Test.Next/Remote/SnapshotSerializationTests.cs @@ -536,7 +536,7 @@ public async Task EmptyAssetChecksumTest() var serializer = document.Project.Solution.Services.GetService(); var text = await document.GetTextAsync().ConfigureAwait(false); - var source = serializer.CreateChecksum(new SerializableSourceText(text, text.GetContentHash()), CancellationToken.None); + var source = new SerializableSourceText(text, text.GetContentHash()).ContentChecksum; var metadata = serializer.CreateChecksum(new MissingMetadataReference(), CancellationToken.None); var analyzer = serializer.CreateChecksum(new AnalyzerFileReference(Path.Combine(TempRoot.Root, "missing"), new MissingAnalyzerLoader()), CancellationToken.None); @@ -611,11 +611,9 @@ public void TestEncodingSerialization() var serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash()); using (var stream = SerializableBytes.CreateWritableStream()) { - using var context = new SolutionReplicationContext(); - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(serializableSourceText, objectWriter, context, CancellationToken.None); + serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None); } stream.Position = 0; @@ -631,11 +629,9 @@ public void TestEncodingSerialization() serializableSourceText = new SerializableSourceText(sourceText, sourceText.GetContentHash()); using (var stream = SerializableBytes.CreateWritableStream()) { - using var context = new SolutionReplicationContext(); - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(serializableSourceText, objectWriter, context, CancellationToken.None); + serializer.Serialize(serializableSourceText, objectWriter, CancellationToken.None); } stream.Position = 0; @@ -662,11 +658,9 @@ public void TestCompilationOptions_NullableAndImport() void VerifyOptions(CompilationOptions originalOptions) { using var stream = SerializableBytes.CreateWritableStream(); - using var context = new SolutionReplicationContext(); - using (var objectWriter = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(originalOptions, objectWriter, context, CancellationToken.None); + serializer.Serialize(originalOptions, objectWriter, CancellationToken.None); } stream.Position = 0; @@ -683,17 +677,17 @@ void VerifyOptions(CompilationOptions originalOptions) private static SolutionAsset CloneAsset(ISerializerService serializer, SolutionAsset asset) { using var stream = SerializableBytes.CreateWritableStream(); - using var context = new SolutionReplicationContext(); - using (var writer = new ObjectWriter(stream, leaveOpen: true)) { - serializer.Serialize(asset.Value, writer, context, CancellationToken.None); + serializer.Serialize(asset.Value, writer, CancellationToken.None); } stream.Position = 0; using var reader = ObjectReader.TryGetReader(stream); var recovered = serializer.Deserialize(asset.Kind, reader, CancellationToken.None); - var assetFromStorage = new SolutionAsset(serializer.CreateChecksum(recovered, CancellationToken.None), recovered); + var checksum = recovered is SerializableSourceText text ? text.ContentChecksum : serializer.CreateChecksum(recovered, CancellationToken.None); + + var assetFromStorage = new SolutionAsset(checksum, recovered); Assert.Equal(asset.Checksum, assetFromStorage.Checksum); return assetFromStorage; diff --git a/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs b/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs index 30732fa083aa0..f9826d7e8aafe 100644 --- a/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/AssetProviderTests.cs @@ -7,6 +7,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -76,7 +77,7 @@ public async Task TestAssetSynchronization() // build checksum await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var map = await solution.GetAssetMapAsync(CancellationToken.None); + var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); using var remoteWorkspace = CreateRemoteWorkspace(); @@ -104,7 +105,7 @@ public async Task TestSolutionSynchronization() // build checksum await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var map = await solution.GetAssetMapAsync(CancellationToken.None); + var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); using var remoteWorkspace = CreateRemoteWorkspace(); @@ -146,5 +147,79 @@ public async Task TestProjectSynchronization() TestUtils.VerifyAssetStorage(map, storage); } + + [Fact] + public async Task TestAssetArrayOrdering() + { + var code1 = @"class Test1 { void Method() { } }"; + var code2 = @"class Test2 { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp([code1, code2]); + var project = workspace.CurrentSolution.Projects.First(); + + await project.State.GetChecksumAsync(CancellationToken.None); + + var map = await project.GetAssetMapAsync(CancellationToken.None); + + using var remoteWorkspace = CreateRemoteWorkspace(); + + var sessionId = Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())); + var storage = new SolutionAssetCache(); + var assetSource = new OrderedAssetSource(workspace.Services.GetService(), map); + + var service = new AssetProvider(sessionId, storage, assetSource, remoteWorkspace.Services.GetService()); + + using var _ = ArrayBuilder.GetInstance(out var allProjectChecksums); + var stateChecksums = await project.State.GetStateChecksumsAsync(CancellationToken.None); + + var textChecksums = stateChecksums.Documents.TextChecksums; + var textChecksumsReversed = new ChecksumCollection(textChecksums.Children.Reverse().ToImmutableArray()); + + var documents = await service.GetAssetsArrayAsync( + AssetPath.FullLookupForTesting, textChecksums, CancellationToken.None); + Assert.True(documents.Length == 2); + + storage.GetTestAccessor().Clear(); + var documentsReversed = await service.GetAssetsArrayAsync( + AssetPath.FullLookupForTesting, textChecksumsReversed, CancellationToken.None); + Assert.True(documentsReversed.Length == 2); + + Assert.True(documents.Select(d => d.ContentChecksum).SequenceEqual(documentsReversed.Reverse().Select(d => d.ContentChecksum))); + } + + private sealed class OrderedAssetSource( + ISerializerService serializerService, + IReadOnlyDictionary map) : IAssetSource + { + public ValueTask GetAssetsAsync( + Checksum solutionChecksum, + AssetPath assetPath, + ReadOnlyMemory checksums, + ISerializerService deserializerService, + Action callback, + TArg arg, + CancellationToken cancellationToken) + { + foreach (var (checksum, asset) in map) + { + if (checksums.Span.IndexOf(checksum) >= 0) + { + using var stream = new MemoryStream(); + using (var writer = new ObjectWriter(stream, leaveOpen: true)) + { + serializerService.Serialize(asset, writer, cancellationToken); + } + + stream.Position = 0; + using var reader = ObjectReader.GetReader(stream, leaveOpen: true); + var deserialized = deserializerService.Deserialize(asset.GetWellKnownSynchronizationKind(), reader, cancellationToken); + Contract.ThrowIfNull(deserialized); + callback(checksum, (T)deserialized, arg); + } + } + + return ValueTaskFactory.CompletedTask; + } + } } } diff --git a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs index 1a9747fc226a7..d73b4bb4f6ee9 100644 --- a/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/ServiceHubServicesTests.cs @@ -91,7 +91,7 @@ public async Task TestRemoteHostTextSynchronize() // sync await client.TryInvokeAsync( - (service, cancellationToken) => service.SynchronizeTextAsync(oldDocument.Id, oldState.Text, newText.GetTextChanges(oldText), cancellationToken), + (service, cancellationToken) => service.SynchronizeTextAsync(oldDocument.Id, oldState.Text, newText.GetTextChanges(oldText).AsImmutable(), cancellationToken), CancellationToken.None); // apply change to solution @@ -1450,7 +1450,7 @@ private static void VerifyStates(Solution solution1, Solution solution2, string private static async Task VerifyAssetStorageAsync(InProcRemoteHostClient client, Solution solution) { - var map = await solution.GetAssetMapAsync(CancellationToken.None); + var map = await solution.GetAssetMapAsync(projectConeId: null, CancellationToken.None); var storage = client.TestData.WorkspaceManager.SolutionAssetCache; @@ -1525,7 +1525,7 @@ private static Solution Populate(Solution solution) ], [ "cs additional file content" - ], solution.ProjectIds.ToArray()); + ], [.. solution.ProjectIds]); solution = AddProject(solution, LanguageNames.CSharp, [ diff --git a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs index 287ddfe63a30b..93583058aead3 100644 --- a/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/SolutionServiceTests.cs @@ -13,912 +13,1153 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces; +using Microsoft.CodeAnalysis.Editor.Test; using Microsoft.CodeAnalysis.Formatting; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Remote; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Shared.Extensions; -using Microsoft.CodeAnalysis.SolutionCrawler; +using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; using Roslyn.Test.Utilities; using Roslyn.Utilities; using Xunit; -namespace Roslyn.VisualStudio.Next.UnitTests.Remote +namespace Roslyn.VisualStudio.Next.UnitTests.Remote; + +[UseExportProvider] +[Trait(Traits.Feature, Traits.Features.RemoteHost)] +public class SolutionServiceTests { - [UseExportProvider] - [Trait(Traits.Feature, Traits.Features.RemoteHost)] - public class SolutionServiceTests + private static readonly TestComposition s_composition = FeaturesTestCompositions.Features.WithTestHostParts(TestHost.OutOfProcess); + private static readonly TestComposition s_compositionWithFirstDocumentIsActiveAndVisible = + s_composition.AddParts(typeof(FirstDocumentIsActiveAndVisibleDocumentTrackingService.Factory)); + + private static RemoteWorkspace CreateRemoteWorkspace() + => new(FeaturesTestCompositions.RemoteHost.GetHostServices()); + + [Fact] + public async Task TestCreation() { - private static RemoteWorkspace CreateRemoteWorkspace() - => new RemoteWorkspace(FeaturesTestCompositions.RemoteHost.GetHostServices()); + var code = @"class Test { void Method() { } }"; - [Fact] - public async Task TestCreation() - { - var code = @"class Test { void Method() { } }"; + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + var solution = workspace.CurrentSolution; + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - var solution = workspace.CurrentSolution; - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + } - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); - } + [Theory] + [CombinatorialData] + public async Task TestGetSolutionWithPrimaryFlag(bool updatePrimaryBranch) + { + var code1 = @"class Test1 { void Method() { } }"; - [Theory] - [CombinatorialData] - public async Task TestGetSolutionWithPrimaryFlag(bool updatePrimaryBranch) - { - var code1 = @"class Test1 { void Method() { } }"; + using var workspace = TestWorkspace.CreateCSharp(code1); + using var remoteWorkspace = CreateRemoteWorkspace(); - using var workspace = TestWorkspace.CreateCSharp(code1); - using var remoteWorkspace = CreateRemoteWorkspace(); + var solution = workspace.CurrentSolution; + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - var solution = workspace.CurrentSolution; - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, cancellationToken: CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, cancellationToken: CancellationToken.None); - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + Assert.Equal(WorkspaceKind.RemoteWorkspace, synched.WorkspaceKind); + } - Assert.Equal(WorkspaceKind.RemoteWorkspace, synched.WorkspaceKind); - } + [Fact] + public async Task TestStrongNameProvider() + { + using var workspace = new AdhocWorkspace(); + using var remoteWorkspace = CreateRemoteWorkspace(); - [Fact] - public async Task TestStrongNameProvider() - { - using var workspace = new AdhocWorkspace(); - using var remoteWorkspace = CreateRemoteWorkspace(); + var filePath = typeof(SolutionServiceTests).Assembly.Location; - var filePath = typeof(SolutionServiceTests).Assembly.Location; + workspace.AddProject( + ProjectInfo.Create( + ProjectId.CreateNewId(), VersionStamp.Create(), "test", "test.dll", LanguageNames.CSharp, + filePath: filePath, outputFilePath: filePath)); - workspace.AddProject( - ProjectInfo.Create( - ProjectId.CreateNewId(), VersionStamp.Create(), "test", "test.dll", LanguageNames.CSharp, - filePath: filePath, outputFilePath: filePath)); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); + var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); + var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); - var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var compilationOptions = solution.Projects.First().CompilationOptions; - var compilationOptions = solution.Projects.First().CompilationOptions; + Assert.IsType(compilationOptions.StrongNameProvider); + Assert.IsType(compilationOptions.XmlReferenceResolver); - Assert.IsType(compilationOptions.StrongNameProvider); - Assert.IsType(compilationOptions.XmlReferenceResolver); + var dirName = PathUtilities.GetDirectoryName(filePath); + var array = new[] { dirName, dirName }; + Assert.Equal(Hash.CombineValues(array, StringComparer.Ordinal), compilationOptions.StrongNameProvider.GetHashCode()); + Assert.Equal(((XmlFileResolver)compilationOptions.XmlReferenceResolver).BaseDirectory, dirName); + } - var dirName = PathUtilities.GetDirectoryName(filePath); - var array = new[] { dirName, dirName }; - Assert.Equal(Hash.CombineValues(array, StringComparer.Ordinal), compilationOptions.StrongNameProvider.GetHashCode()); - Assert.Equal(((XmlFileResolver)compilationOptions.XmlReferenceResolver).BaseDirectory, dirName); - } + [Fact] + public async Task TestStrongNameProviderEmpty() + { + using var workspace = new AdhocWorkspace(); + using var remoteWorkspace = CreateRemoteWorkspace(); - [Fact] - public async Task TestStrongNameProviderEmpty() - { - using var workspace = new AdhocWorkspace(); - using var remoteWorkspace = CreateRemoteWorkspace(); + var filePath = "testLocation"; - var filePath = "testLocation"; + workspace.AddProject( + ProjectInfo.Create( + ProjectId.CreateNewId(), VersionStamp.Create(), "test", "test.dll", LanguageNames.CSharp, + filePath: filePath, outputFilePath: filePath)); - workspace.AddProject( - ProjectInfo.Create( - ProjectId.CreateNewId(), VersionStamp.Create(), "test", "test.dll", LanguageNames.CSharp, - filePath: filePath, outputFilePath: filePath)); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, workspace.CurrentSolution); + var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); + var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - var solutionChecksum = await workspace.CurrentSolution.CompilationState.GetChecksumAsync(CancellationToken.None); - var solution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var compilationOptions = solution.Projects.First().CompilationOptions; - var compilationOptions = solution.Projects.First().CompilationOptions; + Assert.True(compilationOptions.StrongNameProvider is DesktopStrongNameProvider); + Assert.True(compilationOptions.XmlReferenceResolver is XmlFileResolver); - Assert.True(compilationOptions.StrongNameProvider is DesktopStrongNameProvider); - Assert.True(compilationOptions.XmlReferenceResolver is XmlFileResolver); + var array = new string[] { }; + Assert.Equal(Hash.CombineValues(array, StringComparer.Ordinal), compilationOptions.StrongNameProvider.GetHashCode()); + Assert.Null(((XmlFileResolver)compilationOptions.XmlReferenceResolver).BaseDirectory); + } - var array = new string[] { }; - Assert.Equal(Hash.CombineValues(array, StringComparer.Ordinal), compilationOptions.StrongNameProvider.GetHashCode()); - Assert.Null(((XmlFileResolver)compilationOptions.XmlReferenceResolver).BaseDirectory); - } + [Fact] + public async Task TestCache() + { + var code = @"class Test { void Method() { } }"; - [Fact] - public async Task TestCache() - { - var code = @"class Test { void Method() { } }"; + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + var solution = workspace.CurrentSolution; + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var solution = workspace.CurrentSolution; - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var first = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var second = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - var first = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - var second = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + // same instance from cache + Assert.True(object.ReferenceEquals(first, second)); + Assert.Equal(WorkspaceKind.RemoteWorkspace, first.WorkspaceKind); + } - // same instance from cache - Assert.True(object.ReferenceEquals(first, second)); - Assert.Equal(WorkspaceKind.RemoteWorkspace, first.WorkspaceKind); - } + [Fact] + public async Task TestUpdatePrimaryWorkspace() + { + var code = @"class Test { void Method() { } }"; - [Fact] - public async Task TestUpdatePrimaryWorkspace() - { - var code = @"class Test { void Method() { } }"; + await VerifySolutionUpdate(code, s => s.WithDocumentText(s.Projects.First().DocumentIds.First(), SourceText.From(code + " "))); + } - await VerifySolutionUpdate(code, s => s.WithDocumentText(s.Projects.First().DocumentIds.First(), SourceText.From(code + " "))); - } + [Fact] + public async Task ProjectProperties() + { + using var workspace = TestWorkspace.CreateCSharp(""); - [Fact] - public async Task ProjectProperties() + static Solution SetProjectProperties(Solution solution, int version) { - using var workspace = TestWorkspace.CreateCSharp(""); - - static Solution SetProjectProperties(Solution solution, int version) - { - var projectId = solution.ProjectIds.Single(); - return solution - .WithProjectName(projectId, "Name" + version) - .WithProjectAssemblyName(projectId, "AssemblyName" + version) - .WithProjectFilePath(projectId, "FilePath" + version) - .WithProjectOutputFilePath(projectId, "OutputFilePath" + version) - .WithProjectOutputRefFilePath(projectId, "OutputRefFilePath" + version) - .WithProjectCompilationOutputInfo(projectId, new CompilationOutputInfo("AssemblyPath" + version)) - .WithProjectDefaultNamespace(projectId, "DefaultNamespace" + version) - .WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithm.Sha1 + version) - .WithHasAllInformation(projectId, (version % 2) != 0) - .WithRunAnalyzers(projectId, (version % 2) != 0); - } - - static void ValidateProperties(Solution solution, int version) - { - var project = solution.Projects.Single(); - Assert.Equal("Name" + version, project.Name); - Assert.Equal("AssemblyName" + version, project.AssemblyName); - Assert.Equal("FilePath" + version, project.FilePath); - Assert.Equal("OutputFilePath" + version, project.OutputFilePath); - Assert.Equal("OutputRefFilePath" + version, project.OutputRefFilePath); - Assert.Equal("AssemblyPath" + version, project.CompilationOutputInfo.AssemblyPath); - Assert.Equal("DefaultNamespace" + version, project.DefaultNamespace); - Assert.Equal(SourceHashAlgorithm.Sha1 + version, project.State.ChecksumAlgorithm); - Assert.Equal((version % 2) != 0, project.State.HasAllInformation); - Assert.Equal((version % 2) != 0, project.State.RunAnalyzers); - } - - Assert.True(workspace.SetCurrentSolution(s => SetProjectProperties(s, version: 0), WorkspaceChangeKind.SolutionChanged)); - - await VerifySolutionUpdate(workspace, - newSolutionGetter: s => SetProjectProperties(s, version: 1), - oldSolutionValidator: s => ValidateProperties(s, version: 0), - newSolutionValidator: s => ValidateProperties(s, version: 1)).ConfigureAwait(false); + var projectId = solution.ProjectIds.Single(); + return solution + .WithProjectName(projectId, "Name" + version) + .WithProjectAssemblyName(projectId, "AssemblyName" + version) + .WithProjectFilePath(projectId, "FilePath" + version) + .WithProjectOutputFilePath(projectId, "OutputFilePath" + version) + .WithProjectOutputRefFilePath(projectId, "OutputRefFilePath" + version) + .WithProjectCompilationOutputInfo(projectId, new CompilationOutputInfo("AssemblyPath" + version)) + .WithProjectDefaultNamespace(projectId, "DefaultNamespace" + version) + .WithProjectChecksumAlgorithm(projectId, SourceHashAlgorithm.Sha1 + version) + .WithHasAllInformation(projectId, (version % 2) != 0) + .WithRunAnalyzers(projectId, (version % 2) != 0); } - [Fact] - public async Task TestUpdateDocumentInfo() + static void ValidateProperties(Solution solution, int version) { - var code = @"class Test { void Method() { } }"; - - await VerifySolutionUpdate(code, s => s.WithDocumentFolders(s.Projects.First().Documents.First().Id, new[] { "test" })); + var project = solution.Projects.Single(); + Assert.Equal("Name" + version, project.Name); + Assert.Equal("AssemblyName" + version, project.AssemblyName); + Assert.Equal("FilePath" + version, project.FilePath); + Assert.Equal("OutputFilePath" + version, project.OutputFilePath); + Assert.Equal("OutputRefFilePath" + version, project.OutputRefFilePath); + Assert.Equal("AssemblyPath" + version, project.CompilationOutputInfo.AssemblyPath); + Assert.Equal("DefaultNamespace" + version, project.DefaultNamespace); + Assert.Equal(SourceHashAlgorithm.Sha1 + version, project.State.ChecksumAlgorithm); + Assert.Equal((version % 2) != 0, project.State.HasAllInformation); + Assert.Equal((version % 2) != 0, project.State.RunAnalyzers); } - [Fact] - public async Task TestAddUpdateRemoveProjects() + Assert.True(workspace.SetCurrentSolution(s => SetProjectProperties(s, version: 0), WorkspaceChangeKind.SolutionChanged)); + + await VerifySolutionUpdate(workspace, + newSolutionGetter: s => SetProjectProperties(s, version: 1), + oldSolutionValidator: s => ValidateProperties(s, version: 0), + newSolutionValidator: s => ValidateProperties(s, version: 1)).ConfigureAwait(false); + } + + [Fact] + public async Task TestUpdateDocumentInfo() + { + var code = @"class Test { void Method() { } }"; + + await VerifySolutionUpdate(code, s => s.WithDocumentFolders(s.Projects.First().Documents.First().Id, new[] { "test" })); + } + + [Fact] + public async Task TestAddUpdateRemoveProjects() + { + var code = @"class Test { void Method() { } }"; + + await VerifySolutionUpdate(code, s => { - var code = @"class Test { void Method() { } }"; + var existingProjectId = s.ProjectIds.First(); - await VerifySolutionUpdate(code, s => - { - var existingProjectId = s.ProjectIds.First(); + s = s.AddProject("newProject", "newProject", LanguageNames.CSharp).Solution; - s = s.AddProject("newProject", "newProject", LanguageNames.CSharp).Solution; + var project = s.GetProject(existingProjectId); + project = project.WithCompilationOptions(project.CompilationOptions.WithModuleName("modified")); - var project = s.GetProject(existingProjectId); - project = project.WithCompilationOptions(project.CompilationOptions.WithModuleName("modified")); + var existingDocumentId = project.DocumentIds.First(); - var existingDocumentId = project.DocumentIds.First(); + project = project.AddDocument("newDocument", SourceText.From("// new text")).Project; - project = project.AddDocument("newDocument", SourceText.From("// new text")).Project; + var document = project.GetDocument(existingDocumentId); - var document = project.GetDocument(existingDocumentId); + document = document.WithSourceCodeKind(SourceCodeKind.Script); - document = document.WithSourceCodeKind(SourceCodeKind.Script); + return document.Project.Solution; + }); + } - return document.Project.Solution; - }); - } + [Fact] + public async Task TestAdditionalDocument() + { + var code = @"class Test { void Method() { } }"; + using var workspace = TestWorkspace.CreateCSharp(code); - [Fact] - public async Task TestAdditionalDocument() + var projectId = workspace.CurrentSolution.ProjectIds.First(); + var additionalDocumentId = DocumentId.CreateNewId(projectId); + var additionalDocumentInfo = DocumentInfo.Create( + additionalDocumentId, "additionalFile", + loader: TextLoader.From(TextAndVersion.Create(SourceText.From("test"), VersionStamp.Create()))); + + await VerifySolutionUpdate(workspace, s => { - var code = @"class Test { void Method() { } }"; - using var workspace = TestWorkspace.CreateCSharp(code); - - var projectId = workspace.CurrentSolution.ProjectIds.First(); - var additionalDocumentId = DocumentId.CreateNewId(projectId); - var additionalDocumentInfo = DocumentInfo.Create( - additionalDocumentId, "additionalFile", - loader: TextLoader.From(TextAndVersion.Create(SourceText.From("test"), VersionStamp.Create()))); - - await VerifySolutionUpdate(workspace, s => - { - return s.AddAdditionalDocument(additionalDocumentInfo); - }); - - workspace.OnAdditionalDocumentAdded(additionalDocumentInfo); - - await VerifySolutionUpdate(workspace, s => - { - return s.WithAdditionalDocumentText(additionalDocumentId, SourceText.From("changed")); - }); - - await VerifySolutionUpdate(workspace, s => - { - return s.RemoveAdditionalDocument(additionalDocumentId); - }); - } + return s.AddAdditionalDocument(additionalDocumentInfo); + }); - [Fact] - public async Task TestAnalyzerConfigDocument() + workspace.OnAdditionalDocumentAdded(additionalDocumentInfo); + + await VerifySolutionUpdate(workspace, s => { - var configPath = Path.Combine(Path.GetTempPath(), ".editorconfig"); - var code = @"class Test { void Method() { } }"; - using var workspace = TestWorkspace.CreateCSharp(code); - - var projectId = workspace.CurrentSolution.ProjectIds.First(); - var analyzerConfigDocumentId = DocumentId.CreateNewId(projectId); - var analyzerConfigDocumentInfo = DocumentInfo.Create( - analyzerConfigDocumentId, - name: ".editorconfig", - loader: TextLoader.From(TextAndVersion.Create(SourceText.From("root = true"), VersionStamp.Create(), filePath: configPath)), - filePath: configPath); - - await VerifySolutionUpdate(workspace, s => - { - return s.AddAnalyzerConfigDocuments(ImmutableArray.Create(analyzerConfigDocumentInfo)); - }); - - workspace.OnAnalyzerConfigDocumentAdded(analyzerConfigDocumentInfo); - - await VerifySolutionUpdate(workspace, s => - { - return s.WithAnalyzerConfigDocumentText(analyzerConfigDocumentId, SourceText.From("root = false")); - }); - - await VerifySolutionUpdate(workspace, s => - { - return s.RemoveAnalyzerConfigDocument(analyzerConfigDocumentId); - }); - } + return s.WithAdditionalDocumentText(additionalDocumentId, SourceText.From("changed")); + }); + + await VerifySolutionUpdate(workspace, s => + { + return s.RemoveAdditionalDocument(additionalDocumentId); + }); + } - [Fact, Trait(Traits.Feature, Traits.Features.RemoteHost)] - public async Task TestDocument() + [Fact] + public async Task TestAnalyzerConfigDocument() + { + var configPath = Path.Combine(Path.GetTempPath(), ".editorconfig"); + var code = @"class Test { void Method() { } }"; + using var workspace = TestWorkspace.CreateCSharp(code); + + var projectId = workspace.CurrentSolution.ProjectIds.First(); + var analyzerConfigDocumentId = DocumentId.CreateNewId(projectId); + var analyzerConfigDocumentInfo = DocumentInfo.Create( + analyzerConfigDocumentId, + name: ".editorconfig", + loader: TextLoader.From(TextAndVersion.Create(SourceText.From("root = true"), VersionStamp.Create(), filePath: configPath)), + filePath: configPath); + + await VerifySolutionUpdate(workspace, s => { - var code = @"class Test { void Method() { } }"; + return s.AddAnalyzerConfigDocuments(ImmutableArray.Create(analyzerConfigDocumentInfo)); + }); - using var workspace = TestWorkspace.CreateCSharp(code); + workspace.OnAnalyzerConfigDocumentAdded(analyzerConfigDocumentInfo); - var projectId = workspace.CurrentSolution.ProjectIds.First(); - var documentId = DocumentId.CreateNewId(projectId); - var documentInfo = DocumentInfo.Create( - documentId, "sourceFile", - loader: TextLoader.From(TextAndVersion.Create(SourceText.From("class A { }"), VersionStamp.Create()))); + await VerifySolutionUpdate(workspace, s => + { + return s.WithAnalyzerConfigDocumentText(analyzerConfigDocumentId, SourceText.From("root = false")); + }); - await VerifySolutionUpdate(workspace, s => - { - return s.AddDocument(documentInfo); - }); + await VerifySolutionUpdate(workspace, s => + { + return s.RemoveAnalyzerConfigDocument(analyzerConfigDocumentId); + }); + } - workspace.OnDocumentAdded(documentInfo); + [Fact, Trait(Traits.Feature, Traits.Features.RemoteHost)] + public async Task TestDocument() + { + var code = @"class Test { void Method() { } }"; - await VerifySolutionUpdate(workspace, s => - { - return s.WithDocumentText(documentId, SourceText.From("class Changed { }")); - }); + using var workspace = TestWorkspace.CreateCSharp(code); - await VerifySolutionUpdate(workspace, s => - { - return s.RemoveDocument(documentId); - }); - } + var projectId = workspace.CurrentSolution.ProjectIds.First(); + var documentId = DocumentId.CreateNewId(projectId); + var documentInfo = DocumentInfo.Create( + documentId, "sourceFile", + loader: TextLoader.From(TextAndVersion.Create(SourceText.From("class A { }"), VersionStamp.Create()))); - [Fact] - public async Task TestRemoteWorkspace() + await VerifySolutionUpdate(workspace, s => { - var code = @"class Test { void Method() { } }"; + return s.AddDocument(documentInfo); + }); - // create base solution - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + workspace.OnDocumentAdded(documentInfo); - // create solution service - var solution1 = workspace.CurrentSolution; - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution1); + await VerifySolutionUpdate(workspace, s => + { + return s.WithDocumentText(documentId, SourceText.From("class Changed { }")); + }); - var remoteSolution1 = await GetInitialOOPSolutionAsync(remoteWorkspace, assetProvider, solution1); + await VerifySolutionUpdate(workspace, s => + { + return s.RemoveDocument(documentId); + }); + } - await Verify(remoteWorkspace, solution1, remoteSolution1); + [Fact] + public async Task TestRemoteWorkspace() + { + var code = @"class Test { void Method() { } }"; - // update remote workspace - var currentSolution = remoteSolution1.WithDocumentText(remoteSolution1.Projects.First().Documents.First().Id, SourceText.From(code + " class Test2 { }")); - var oopSolution2 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); + // create base solution + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - await Verify(remoteWorkspace, currentSolution, oopSolution2); + // create solution service + var solution1 = workspace.CurrentSolution; + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution1); - // move backward - await Verify(remoteWorkspace, remoteSolution1, await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(remoteSolution1)); + var remoteSolution1 = await GetInitialOOPSolutionAsync(remoteWorkspace, assetProvider, solution1); - // move forward - currentSolution = oopSolution2.WithDocumentText(oopSolution2.Projects.First().Documents.First().Id, SourceText.From(code + " class Test3 { }")); - var remoteSolution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); + await Verify(remoteWorkspace, solution1, remoteSolution1); - await Verify(remoteWorkspace, currentSolution, remoteSolution3); + // update remote workspace + var currentSolution = remoteSolution1.WithDocumentText(remoteSolution1.Projects.First().Documents.First().Id, SourceText.From(code + " class Test2 { }")); + var oopSolution2 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); - // move to new solution backward - var solutionInfo2 = await assetProvider.CreateSolutionInfoAsync(await solution1.CompilationState.GetChecksumAsync(CancellationToken.None), CancellationToken.None); - var solution2 = remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo2); + await Verify(remoteWorkspace, currentSolution, oopSolution2); - // move to new solution forward - var solution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(solution2); - Assert.NotNull(solution3); - await Verify(remoteWorkspace, solution1, solution3); + // move backward + await Verify(remoteWorkspace, remoteSolution1, await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(remoteSolution1)); - static async Task GetInitialOOPSolutionAsync(RemoteWorkspace remoteWorkspace, AssetProvider assetProvider, Solution solution) - { - // set up initial solution - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); + // move forward + currentSolution = oopSolution2.WithDocumentText(oopSolution2.Projects.First().Documents.First().Id, SourceText.From(code + " class Test3 { }")); + var remoteSolution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(currentSolution); - // get solution in remote host - return await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - } + await Verify(remoteWorkspace, currentSolution, remoteSolution3); - static async Task Verify(RemoteWorkspace remoteWorkspace, Solution givenSolution, Solution remoteSolution) - { - // verify we got solution expected - Assert.Equal(await givenSolution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + // move to new solution backward + var solutionInfo2 = await assetProvider.CreateSolutionInfoAsync(await solution1.CompilationState.GetChecksumAsync(CancellationToken.None), CancellationToken.None); + var solution2 = remoteWorkspace.GetTestAccessor().CreateSolutionFromInfo(solutionInfo2); - // verify remote workspace got updated - Assert.Equal(remoteSolution, remoteWorkspace.CurrentSolution); - } - } + // move to new solution forward + var solution3 = await remoteWorkspace.GetTestAccessor().UpdateWorkspaceCurrentSolutionAsync(solution2); + Assert.NotNull(solution3); + await Verify(remoteWorkspace, solution1, solution3); - [Theory, CombinatorialData] - [WorkItem("https://github.com/dotnet/roslyn/issues/48564")] - public async Task TestAddingProjectsWithExplicitOptions(bool useDefaultOptionValue) + static async Task GetInitialOOPSolutionAsync(RemoteWorkspace remoteWorkspace, AssetProvider assetProvider, Solution solution) { - using var workspace = TestWorkspace.CreateCSharp(@"public class C { }"); - using var remoteWorkspace = CreateRemoteWorkspace(); - - // Initial empty solution - var solution = workspace.CurrentSolution; - solution = solution.RemoveProject(solution.ProjectIds.Single()); - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + // set up initial solution var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); - - // Add a C# project and a VB project, set some options, and check again - var csharpDocument = new TestHostDocument("public class C { }"); - var csharpProject = new TestHostProject(workspace, csharpDocument, language: LanguageNames.CSharp, name: "project2"); - var csharpProjectInfo = csharpProject.ToProjectInfo(); - - var vbDocument = new TestHostDocument("Public Class D \r\n Inherits C\r\nEnd Class"); - var vbProject = new TestHostProject(workspace, vbDocument, language: LanguageNames.VisualBasic, name: "project3"); - var vbProjectInfo = vbProject.ToProjectInfo(); - - solution = solution.AddProject(csharpProjectInfo).AddProject(vbProjectInfo); - var newOptionValue = useDefaultOptionValue - ? FormattingOptions2.NewLine.DefaultValue - : FormattingOptions2.NewLine.DefaultValue + FormattingOptions2.NewLine.DefaultValue; - solution = solution.WithOptions(solution.Options - .WithChangedOption(FormattingOptions.NewLine, LanguageNames.CSharp, newOptionValue) - .WithChangedOption(FormattingOptions.NewLine, LanguageNames.VisualBasic, newOptionValue)); - - assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); + + // get solution in remote host + return await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); } - [Fact] - public async Task TestFrozenSourceGeneratedDocument() + static async Task Verify(RemoteWorkspace remoteWorkspace, Solution givenSolution, Solution remoteSolution) { - using var workspace = TestWorkspace.CreateCSharp(@""); - using var remoteWorkspace = CreateRemoteWorkspace(); + // verify we got solution expected + Assert.Equal(await givenSolution.CompilationState.GetChecksumAsync(CancellationToken.None), await remoteSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); - var solution = workspace.CurrentSolution - .Projects.Single() - .AddAnalyzerReference(new AnalyzerFileReference(typeof(Microsoft.CodeAnalysis.TestSourceGenerator.HelloWorldGenerator).Assembly.Location, new TestAnalyzerAssemblyLoader())) - .Solution; - - // First sync the solution over that has a generator - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); - - // Now freeze with some content - var documentIdentity = (await solution.Projects.Single().GetSourceGeneratedDocumentsAsync()).First().Identity; - var frozenText1 = SourceText.From("// Hello, World!"); - var frozenSolution1 = solution.WithFrozenSourceGeneratedDocument(documentIdentity, DateTime.Now, frozenText1).Project.Solution; - - assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution1); - solutionChecksum = await frozenSolution1.CompilationState.GetChecksumAsync(CancellationToken.None); - synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); - - // Try freezing with some different content from the original solution - var frozenText2 = SourceText.From("// Hello, World! A second time!"); - var frozenSolution2 = solution.WithFrozenSourceGeneratedDocument(documentIdentity, DateTime.Now, frozenText2).Project.Solution; - - assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution2); - solutionChecksum = await frozenSolution2.CompilationState.GetChecksumAsync(CancellationToken.None); - synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + // verify remote workspace got updated + Assert.Equal(remoteSolution, remoteWorkspace.CurrentSolution); } + } - [Fact] - public async Task TestPartialProjectSync_GetSolutionFirst() - { - var code = @"class Test { void Method() { } }"; + [Theory, CombinatorialData] + [WorkItem("https://github.com/dotnet/roslyn/issues/48564")] + public async Task TestAddingProjectsWithExplicitOptions(bool useDefaultOptionValue) + { + using var workspace = TestWorkspace.CreateCSharp(@"public class C { }"); + using var remoteWorkspace = CreateRemoteWorkspace(); + + // Initial empty solution + var solution = workspace.CurrentSolution; + solution = solution.RemoveProject(solution.ProjectIds.Single()); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + + // Add a C# project and a VB project, set some options, and check again + var csharpDocument = new TestHostDocument("public class C { }"); + var csharpProject = new TestHostProject(workspace, csharpDocument, language: LanguageNames.CSharp, name: "project2"); + var csharpProjectInfo = csharpProject.ToProjectInfo(); + + var vbDocument = new TestHostDocument("Public Class D \r\n Inherits C\r\nEnd Class"); + var vbProject = new TestHostProject(workspace, vbDocument, language: LanguageNames.VisualBasic, name: "project3"); + var vbProjectInfo = vbProject.ToProjectInfo(); + + solution = solution.AddProject(csharpProjectInfo).AddProject(vbProjectInfo); + var newOptionValue = useDefaultOptionValue + ? FormattingOptions2.NewLine.DefaultValue + : FormattingOptions2.NewLine.DefaultValue + FormattingOptions2.NewLine.DefaultValue; + solution = solution.WithOptions(solution.Options + .WithChangedOption(FormattingOptions.NewLine, LanguageNames.CSharp, newOptionValue) + .WithChangedOption(FormattingOptions.NewLine, LanguageNames.VisualBasic, newOptionValue)); + + assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + } - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + [Fact] + public async Task TestFrozenSourceGeneratedDocument() + { + using var workspace = TestWorkspace.CreateCSharp(@""); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution + .Projects.Single() + .AddAnalyzerReference(new AnalyzerFileReference(typeof(Microsoft.CodeAnalysis.TestSourceGenerator.HelloWorldGenerator).Assembly.Location, new TestAnalyzerAssemblyLoader())) + .Solution; + + // First sync the solution over that has a generator + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + + // Now freeze with some content + var documentIdentity = (await solution.Projects.Single().GetSourceGeneratedDocumentsAsync()).First().Identity; + var frozenText1 = SourceText.From("// Hello, World!"); + var frozenSolution1 = solution.WithFrozenSourceGeneratedDocument(documentIdentity, DateTime.Now, frozenText1).Project.Solution; + + assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution1); + solutionChecksum = await frozenSolution1.CompilationState.GetChecksumAsync(CancellationToken.None); + synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + + // Try freezing with some different content from the original solution + var frozenText2 = SourceText.From("// Hello, World! A second time!"); + var frozenSolution2 = solution.WithFrozenSourceGeneratedDocument(documentIdentity, DateTime.Now, frozenText2).Project.Solution; + + assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, frozenSolution2); + solutionChecksum = await frozenSolution2.CompilationState.GetChecksumAsync(CancellationToken.None); + synched = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(solutionChecksum, await synched.CompilationState.GetChecksumAsync(CancellationToken.None)); + } - var solution = workspace.CurrentSolution; + [Fact] + public async Task TestPartialProjectSync_GetSolutionFirst() + { + var code = @"class Test { void Method() { } }"; - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - solution = project2.Solution; + var solution = workspace.CurrentSolution; - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - // Do the initial full sync - await solution.AppendAssetMapAsync(map, CancellationToken.None); + solution = project2.Solution; - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); - Assert.Equal(2, syncedFullSolution.Projects.Count()); + // Do the initial full sync + await solution.AppendAssetMapAsync(map, CancellationToken.None); - // Syncing project1 should do nothing as syncing the solution already synced it over. - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project1SyncedSolution.Projects.Count()); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - // Syncing project2 should do nothing as syncing the solution already synced it over. - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project2SyncedSolution.Projects.Count()); - } + Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + Assert.Equal(2, syncedFullSolution.Projects.Count()); - [Fact] - public async Task TestPartialProjectSync_GetSolutionLast() - { - var code = @"class Test { void Method() { } }"; + // Syncing project1 should do nothing as syncing the solution already synced it over. + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project1SyncedSolution.Projects.Count()); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + // Syncing project2 should do nothing as syncing the solution already synced it over. + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); + } - var solution = workspace.CurrentSolution; + [Fact] + public async Task TestPartialProjectSync_GetSolutionLast() + { + var code = @"class Test { void Method() { } }"; - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - solution = project2.Solution; + var solution = workspace.CurrentSolution; - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - // Syncing project 1 should just since it over. - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); - Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); + solution = project2.Solution; - // Syncing project 2 should end up with only p2 synced over. - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - // then syncing the whole project should now copy both over. - await solution.AppendAssetMapAsync(map, CancellationToken.None); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + // Syncing project 1 should just since it over. + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project1SyncedSolution.Projects.Count()); + Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); - Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); - Assert.Equal(2, syncedFullSolution.Projects.Count()); - } + // Syncing project 2 should end up with only p2 synced over. + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project2SyncedSolution.Projects.Count()); - [Fact] - public async Task TestPartialProjectSync_GetDependentProjects1() - { - var code = @"class Test { void Method() { } }"; + // then syncing the whole project should now copy both over. + await solution.AppendAssetMapAsync(map, CancellationToken.None); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var syncedFullSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + Assert.Equal(solutionChecksum, await syncedFullSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + Assert.Equal(2, syncedFullSolution.Projects.Count()); + } - var solution = workspace.CurrentSolution; + [Fact] + public async Task TestPartialProjectSync_GetDependentProjects1() + { + var code = @"class Test { void Method() { } }"; - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); + var solution = workspace.CurrentSolution; - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); - Assert.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); - - // syncing project 3 should sync project 2 as well because of the p2p ref - await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); - var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project3SyncedSolution.Projects.Count()); - } + solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); - [Fact] - public async Task TestPartialProjectSync_GetDependentProjects2() - { - var code = @"class Test { void Method() { } }"; + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project2SyncedSolution.Projects.Count()); + Assert.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); - var solution = workspace.CurrentSolution; + // syncing project 3 should sync project 2 as well because of the p2p ref + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project3SyncedSolution.Projects.Count()); + } - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); + [Fact] + public async Task TestPartialProjectSync_GetDependentProjects2() + { + var code = @"class Test { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution; + + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); + + solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); + + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + + // syncing P3 should since project P2 as well because of the p2p ref + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project3SyncedSolution.Projects.Count()); + + // if we then sync just P2, we should still have only P2 in the synced cone + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project2SyncedSolution.Projects.Count()); + AssertEx.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); + + // if we then sync just P1, we should only have it in its own cone. + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project1SyncedSolution.Projects.Count()); + AssertEx.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); + } - solution = project3.Solution.AddProjectReference(project3.Id, new(project3.Solution.Projects.Single(p => p.Name == "P2").Id)); + [Fact] + public async Task TestPartialProjectSync_GetDependentProjects3() + { + var code = @"class Test { void Method() { } }"; - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - // syncing P3 should since project P2 as well because of the p2p ref - await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); - var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project3SyncedSolution.Projects.Count()); + var solution = workspace.CurrentSolution; - // if we then sync just P2, we should still have only P2 in the synced cone - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); - AssertEx.Equal(project2.Name, project2SyncedSolution.Projects.Single().Name); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); - // if we then sync just P1, we should only have it in its own cone. - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); - AssertEx.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); - } + solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) + .AddProjectReference(project2.Id, new(project1.Id)); - [Fact] - public async Task TestPartialProjectSync_GetDependentProjects3() - { - var code = @"class Test { void Method() { } }"; + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + // syncing project3 should since project2 and project1 as well because of the p2p ref + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(3, project3SyncedSolution.Projects.Count()); - var solution = workspace.CurrentSolution; + // syncing project2 should only have it and project 1. + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); + // syncing project1 should only be itself + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project1SyncedSolution.Projects.Count()); + } - solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) - .AddProjectReference(project2.Id, new(project1.Id)); + [Fact] + public async Task TestPartialProjectSync_GetDependentProjects4() + { + var code = @"class Test { void Method() { } }"; - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - // syncing project3 should since project2 and project1 as well because of the p2p ref - await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); - var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(3, project3SyncedSolution.Projects.Count()); + var solution = workspace.CurrentSolution; - // syncing project2 should only have it and project 1. - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project2SyncedSolution.Projects.Count()); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); - // syncing project1 should only be itself - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); - } + solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) + .AddProjectReference(project3.Id, new(project1.Id)); - [Fact] - public async Task TestPartialProjectSync_GetDependentProjects4() - { - var code = @"class Test { void Method() { } }"; + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + // syncing project3 should since project2 and project1 as well because of the p2p ref + await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); + var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); + var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(3, project3SyncedSolution.Projects.Count()); - var solution = workspace.CurrentSolution; + // Syncing project2 should only have a cone with itself. + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project2SyncedSolution.Projects.Count()); - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - var project3 = project2.Solution.AddProject("P3", "P3", LanguageNames.CSharp); + // Syncing project1 should only have a cone with itself. + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project1SyncedSolution.Projects.Count()); + } - solution = project3.Solution.AddProjectReference(project3.Id, new(project2.Id)) - .AddProjectReference(project3.Id, new(project1.Id)); + [Fact] + public async Task TestPartialProjectSync_Options1() + { + var code = @"class Test { void Method() { } }"; - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - // syncing project3 should since project2 and project1 as well because of the p2p ref - await solution.AppendAssetMapAsync(map, project3.Id, CancellationToken.None); - var project3Checksum = await solution.CompilationState.GetChecksumAsync(project3.Id, CancellationToken.None); - var project3SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project3Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(3, project3SyncedSolution.Projects.Count()); + var solution = workspace.CurrentSolution; - // Syncing project2 should only have a cone with itself. - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.VisualBasic); + + solution = project2.Solution; + + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + + // Syncing over project1 should give us 1 set of options on the OOP side. + await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project1SyncedSolution.Projects.Count()); + Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); + + // Syncing over project2 should also only be one set of options. + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(1, project2SyncedSolution.Projects.Count()); + } + + [Fact] + public async Task TestPartialProjectSync_DoesNotSeeChangesOutsideOfCone() + { + var code = @"class Test { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution; + + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.VisualBasic); + + solution = project2.Solution; + + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - // Syncing project1 should only have a cone with itself. + // Do the initial full sync + await solution.AppendAssetMapAsync(map, CancellationToken.None); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(2, fullSyncedSolution.Projects.Count()); + + // Mutate both projects to each have a document in it. + solution = solution.GetProject(project1.Id).AddDocument("X.cs", SourceText.From("// X")).Project.Solution; + solution = solution.GetProject(project2.Id).AddDocument("Y.vb", SourceText.From("' Y")).Project.Solution; + + // Now just sync project1's cone over. We should not see the change to project2 on the remote side. + // But we will still see project2. + { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); + Assert.Equal(2, project1SyncedSolution.Projects.Count()); + var csharpProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); + var vbProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); + Assert.True(csharpProject.DocumentIds.Count == 2); + Assert.Empty(vbProject.DocumentIds); } - [Fact] - public async Task TestPartialProjectSync_Options1() + // Similarly, if we sync just project2's cone over: { - var code = @"class Test { void Method() { } }"; + await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); + var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); + var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); + Assert.Equal(2, project2SyncedSolution.Projects.Count()); + var csharpProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); + var vbProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); + Assert.Single(csharpProject.DocumentIds); + Assert.Single(vbProject.DocumentIds); + } + } + + [Fact] + public async Task TestPartialProjectSync_AddP2PRef() + { + var code = @"class Test { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution; - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + var project1 = solution.Projects.Single(); + var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); - var solution = workspace.CurrentSolution; + solution = project2.Solution; - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.VisualBasic); + var map = new Dictionary(); + var assetProvider = new AssetProvider( + Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); - solution = project2.Solution; + // Do the initial full sync + await solution.AppendAssetMapAsync(map, CancellationToken.None); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); + Assert.Equal(2, fullSyncedSolution.Projects.Count()); - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + // Mutate both projects to have a document in it, and add a p2p ref from project1 to project2 + solution = solution.GetProject(project1.Id).AddDocument("X.cs", SourceText.From("// X")).Project.Solution; + solution = solution.GetProject(project2.Id).AddDocument("Y.cs", SourceText.From("// Y")).Project.Solution; + solution = solution.GetProject(project1.Id).AddProjectReference(new ProjectReference(project2.Id)).Solution; - // Syncing over project1 should give us 1 set of options on the OOP side. + // Now just sync project1's cone over. This will validate that the p2p ref doesn't try to add a new + // project, but instead sees the existing one. + { await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project1SyncedSolution.Projects.Count()); - Assert.Equal(project1.Name, project1SyncedSolution.Projects.Single().Name); + Assert.Equal(2, project1SyncedSolution.Projects.Count()); + var project1Synced = project1SyncedSolution.GetRequiredProject(project1.Id); + var project2Synced = project1SyncedSolution.GetRequiredProject(project2.Id); - // Syncing over project2 should also only be one set of options. - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(1, project2SyncedSolution.Projects.Count()); + Assert.True(project1Synced.DocumentIds.Count == 2); + Assert.Single(project2Synced.DocumentIds); + Assert.Single(project1Synced.ProjectReferences); } + } - [Fact] - public async Task TestPartialProjectSync_DoesNotSeeChangesOutsideOfCone() - { - var code = @"class Test { void Method() { } }"; + [Fact] + public async Task TestPartialProjectSync_ReferenceToNonExistentProject() + { + var code = @"class Test { void Method() { } }"; - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - var solution = workspace.CurrentSolution; + var solution = workspace.CurrentSolution; - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.VisualBasic); + var project1 = solution.Projects.Single(); - solution = project2.Solution; + // This reference a project that doesn't exist. + // Ensure that it's still fine to get the checksum for this project we have. + project1 = project1.AddProjectReference(new ProjectReference(ProjectId.CreateNewId())); - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + solution = project1.Solution; - // Do the initial full sync - await solution.AppendAssetMapAsync(map, CancellationToken.None); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(2, fullSyncedSolution.Projects.Count()); - - // Mutate both projects to each have a document in it. - solution = solution.GetProject(project1.Id).AddDocument("X.cs", SourceText.From("// X")).Project.Solution; - solution = solution.GetProject(project2.Id).AddDocument("Y.vb", SourceText.From("' Y")).Project.Solution; - - // Now just sync project1's cone over. We should not see the change to project2 on the remote side. - // But we will still see project2. - { - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project1SyncedSolution.Projects.Count()); - var csharpProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); - var vbProject = project1SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); - Assert.True(csharpProject.DocumentIds.Count == 2); - Assert.Empty(vbProject.DocumentIds); - } - - // Similarly, if we sync just project2's cone over: - { - await solution.AppendAssetMapAsync(map, project2.Id, CancellationToken.None); - var project2Checksum = await solution.CompilationState.GetChecksumAsync(project2.Id, CancellationToken.None); - var project2SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project2Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project2SyncedSolution.Projects.Count()); - var csharpProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.CSharp); - var vbProject = project2SyncedSolution.Projects.Single(p => p.Language == LanguageNames.VisualBasic); - Assert.Single(csharpProject.DocumentIds); - Assert.Single(vbProject.DocumentIds); - } - } + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - [Fact] - public async Task TestPartialProjectSync_AddP2PRef() - { - var code = @"class Test { void Method() { } }"; + var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); + } - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + [Fact] + public void TestNoActiveDocumentSemanticModelNotCached() + { + var code = @"class Test { void Method() { } }"; - var solution = workspace.CurrentSolution; + using var workspace = TestWorkspace.CreateCSharp(code); + using var remoteWorkspace = CreateRemoteWorkspace(); - var project1 = solution.Projects.Single(); - var project2 = solution.AddProject("P2", "P2", LanguageNames.CSharp); + var solution = workspace.CurrentSolution; - solution = project2.Solution; + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.Single(); - var map = new Dictionary(); - var assetProvider = new AssetProvider( - Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())), new SolutionAssetCache(), new SimpleAssetSource(workspace.Services.GetService(), map), remoteWorkspace.Services.GetService()); + // Without anything holding onto the semantic model, it should get releases. + var objectReference = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); - // Do the initial full sync - await solution.AppendAssetMapAsync(map, CancellationToken.None); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); - var fullSyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: true, CancellationToken.None); - Assert.Equal(2, fullSyncedSolution.Projects.Count()); - - // Mutate both projects to have a document in it, and add a p2p ref from project1 to project2 - solution = solution.GetProject(project1.Id).AddDocument("X.cs", SourceText.From("// X")).Project.Solution; - solution = solution.GetProject(project2.Id).AddDocument("Y.cs", SourceText.From("// Y")).Project.Solution; - solution = solution.GetProject(project1.Id).AddProjectReference(new ProjectReference(project2.Id)).Solution; - - // Now just sync project1's cone over. This will validate that the p2p ref doesn't try to add a new - // project, but instead sees the existing one. - { - await solution.AppendAssetMapAsync(map, project1.Id, CancellationToken.None); - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - var project1SyncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, project1Checksum, updatePrimaryBranch: false, CancellationToken.None); - Assert.Equal(2, project1SyncedSolution.Projects.Count()); - var project1Synced = project1SyncedSolution.GetRequiredProject(project1.Id); - var project2Synced = project1SyncedSolution.GetRequiredProject(project2.Id); - - Assert.True(project1Synced.DocumentIds.Count == 2); - Assert.Single(project2Synced.DocumentIds); - Assert.Single(project1Synced.ProjectReferences); - } - } + objectReference.AssertReleased(); + } - [Fact] - public async Task TestPartialProjectSync_ReferenceToNonExistentProject() - { - var code = @"class Test { void Method() { } }"; + [Fact] + public void TestActiveDocumentSemanticModelCached() + { + var code = @"class Test { void Method() { } }"; - using var workspace = TestWorkspace.CreateCSharp(code); - using var remoteWorkspace = CreateRemoteWorkspace(); + using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); + using var remoteWorkspace = CreateRemoteWorkspace(); - var solution = workspace.CurrentSolution; + var solution = workspace.CurrentSolution; - var project1 = solution.Projects.Single(); + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.Single(); - // This reference a project that doesn't exist. - // Ensure that it's still fine to get the checksum for this project we have. - project1 = project1.AddProjectReference(new ProjectReference(ProjectId.CreateNewId())); + // Since this is the active document, we should hold onto it. + var objectReference = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); - solution = project1.Solution; + objectReference.AssertHeld(); + } - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + [Fact] + public void TestOnlyActiveDocumentSemanticModelCached() + { + using var workspace = TestWorkspace.Create(""" + + + + class Program1 + { + } + + + class Program2 + { + } + + + + """, composition: s_compositionWithFirstDocumentIsActiveAndVisible); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution; + + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.First(); + var document2 = project1.Documents.Last(); + + // Only the semantic model for the active document should be cached. + var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); + var objectReference2 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); + + objectReference1.AssertHeld(); + objectReference2.AssertReleased(); + } - var project1Checksum = await solution.CompilationState.GetChecksumAsync(project1.Id, CancellationToken.None); - } + [Fact] + public async Task TestRemoteWorkspaceCachesNothingIfActiveDocumentNotSynced() + { + var code = @"class Test { void Method() { } }"; - private static async Task VerifySolutionUpdate(string code, Func newSolutionGetter) - { - using var workspace = TestWorkspace.CreateCSharp(code); - await VerifySolutionUpdate(workspace, newSolutionGetter); - } + using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); + using var remoteWorkspace = CreateRemoteWorkspace(); - private static async Task VerifySolutionUpdate( - TestWorkspace workspace, - Func newSolutionGetter, - Action oldSolutionValidator = null, - Action newSolutionValidator = null) - { - var solution = workspace.CurrentSolution; - oldSolutionValidator?.Invoke(solution); + var solution = workspace.CurrentSolution; - var map = new Dictionary(); + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.Single(); - using var remoteWorkspace = CreateRemoteWorkspace(); - var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution, map); - var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + // Locally the semantic model will be held + var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference1.AssertHeld(); - // update primary workspace - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); - var recoveredSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - oldSolutionValidator?.Invoke(recoveredSolution); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); - Assert.Equal(WorkspaceKind.RemoteWorkspace, recoveredSolution.WorkspaceKind); - Assert.Equal(solutionChecksum, await recoveredSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); - // get new solution - var newSolution = newSolutionGetter(solution); - var newSolutionChecksum = await newSolution.CompilationState.GetChecksumAsync(CancellationToken.None); - await newSolution.AppendAssetMapAsync(map, CancellationToken.None); + // The remote semantic model will not be held as it doesn't know what the active document is yet. + var objectReference2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference2.AssertReleased(); + } - // get solution without updating primary workspace - var recoveredNewSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + [Theory, CombinatorialData] + public async Task TestRemoteWorkspaceCachesPropertyIfActiveDocumentIsSynced(bool updatePrimaryBranch) + { + var code = @"class Test { void Method() { } }"; - Assert.Equal(newSolutionChecksum, await recoveredNewSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); + using var remoteWorkspace = CreateRemoteWorkspace(); - // do same once updating primary workspace - await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, newSolutionChecksum, CancellationToken.None); - var third = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + var solution = workspace.CurrentSolution; - Assert.Equal(newSolutionChecksum, await third.CompilationState.GetChecksumAsync(CancellationToken.None)); + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.Single(); - newSolutionValidator?.Invoke(recoveredNewSolution); - } + // Locally the semantic model will be held + var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference1.AssertHeld(); - private static async Task GetAssetProviderAsync(Workspace workspace, RemoteWorkspace remoteWorkspace, Solution solution, Dictionary map = null) - { - // make sure checksum is calculated - await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + var remoteDocumentTrackingService = (RemoteDocumentTrackingService)remoteWorkspace.Services.GetRequiredService(); + remoteDocumentTrackingService.SetActiveDocument(document1.Id); - map ??= []; - await solution.AppendAssetMapAsync(map, CancellationToken.None); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, CancellationToken.None); - var sessionId = Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())); - var storage = new SolutionAssetCache(); - var assetSource = new SimpleAssetSource(workspace.Services.GetService(), map); + // The remote semantic model will be held as it refers to the active document. + var objectReference2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference2.AssertHeld(); + } - return new AssetProvider(sessionId, storage, assetSource, remoteWorkspace.Services.GetService()); - } + [Theory, CombinatorialData] + public async Task ValidateUpdaterInformsRemoteWorkspaceOfActiveDocument(bool updatePrimaryBranch) + { + var code = @"class Test { void Method() { } }"; + + using var workspace = TestWorkspace.CreateCSharp(code, composition: s_compositionWithFirstDocumentIsActiveAndVisible); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution; + + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.Single(); + + // Locally the semantic model will be held + var objectReference1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference1.AssertHeld(); + + // By creating a checksum updater, we should notify the remote workspace of the active document. + var listenerProvider = workspace.ExportProvider.GetExportedValue(); + var checksumUpdater = new SolutionChecksumUpdater(workspace, listenerProvider, CancellationToken.None); + + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, CancellationToken.None); + + var waiter = listenerProvider.GetWaiter(FeatureAttribute.SolutionChecksumUpdater); + await waiter.ExpeditedWaitAsync(); + + // The remote semantic model will be held as it refers to the active document. + var objectReference2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference2.AssertHeld(); + } + + [Theory, CombinatorialData] + public async Task ValidateUpdaterInformsRemoteWorkspaceOfActiveDocument_EvenAcrossActiveDocumentChanges(bool updatePrimaryBranch) + { + using var workspace = TestWorkspace.Create(""" + + + + class Program1 + { + } + + + class Program2 + { + } + + + + """, composition: s_composition.AddParts(typeof(TestDocumentTrackingService))); + using var remoteWorkspace = CreateRemoteWorkspace(); + + var solution = workspace.CurrentSolution; + + var project1 = solution.Projects.Single(); + var document1 = project1.Documents.First(); + var document2 = project1.Documents.Last(); + + // By creating a checksum updater, we should notify the remote workspace of the active document. Have it + // initially be set to the first document. + var documentTrackingService = (TestDocumentTrackingService)workspace.Services.GetRequiredService(); + documentTrackingService.SetActiveDocument(document1.Id); + + // Locally the semantic model for the first document will be held, but the second will not. + var objectReference1_step1 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); + var objectReference2_step1 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); + objectReference1_step1.AssertHeld(); + objectReference2_step1.AssertReleased(); + + var listenerProvider = workspace.ExportProvider.GetExportedValue(); + var checksumUpdater = new SolutionChecksumUpdater(workspace, listenerProvider, CancellationToken.None); + + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution); + + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + var syncedSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch, CancellationToken.None); + + var waiter = listenerProvider.GetWaiter(FeatureAttribute.SolutionChecksumUpdater); + await waiter.ExpeditedWaitAsync(); + + // The remote semantic model should match the local behavior once it has been notified that the first document is active. + var oopDocumentReference1_step1 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + var oopDocumentReference2_step1 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document2.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + oopDocumentReference1_step1.AssertHeld(); + oopDocumentReference2_step1.AssertReleased(); + + // Now, change the active document to the second document. + documentTrackingService.SetActiveDocument(document2.Id); + + // And get the semantic models again. The second document should now be held, and the first released. + var objectReference1_step2 = ObjectReference.CreateFromFactory(() => document1.GetSemanticModelAsync().GetAwaiter().GetResult()); + var objectReference2_step2 = ObjectReference.CreateFromFactory(() => document2.GetSemanticModelAsync().GetAwaiter().GetResult()); + + // The second document should be held. + objectReference2_step2.AssertHeld(); + + // Ensure that the active doc change is sync'ed to oop. + await waiter.ExpeditedWaitAsync(); + + // And get the semantic models again on the oop side. The second document should now be held, and the first released. + var oopDocumentReference1_step2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document1.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + var oopDocumentReference2_step2 = ObjectReference.CreateFromFactory(() => syncedSolution.GetRequiredDocument(document2.Id).GetSemanticModelAsync().GetAwaiter().GetResult()); + + // The second document on oop should now be held. + oopDocumentReference2_step2.AssertHeld(); + } + + private static async Task VerifySolutionUpdate(string code, Func newSolutionGetter) + { + using var workspace = TestWorkspace.CreateCSharp(code); + await VerifySolutionUpdate(workspace, newSolutionGetter); + } + + private static async Task VerifySolutionUpdate( + TestWorkspace workspace, + Func newSolutionGetter, + Action oldSolutionValidator = null, + Action newSolutionValidator = null) + { + var solution = workspace.CurrentSolution; + oldSolutionValidator?.Invoke(solution); + + var map = new Dictionary(); + + using var remoteWorkspace = CreateRemoteWorkspace(); + var assetProvider = await GetAssetProviderAsync(workspace, remoteWorkspace, solution, map); + var solutionChecksum = await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + + // update primary workspace + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, CancellationToken.None); + var recoveredSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, solutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + oldSolutionValidator?.Invoke(recoveredSolution); + + Assert.Equal(WorkspaceKind.RemoteWorkspace, recoveredSolution.WorkspaceKind); + Assert.Equal(solutionChecksum, await recoveredSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + + // get new solution + var newSolution = newSolutionGetter(solution); + var newSolutionChecksum = await newSolution.CompilationState.GetChecksumAsync(CancellationToken.None); + await newSolution.AppendAssetMapAsync(map, CancellationToken.None); + + // get solution without updating primary workspace + var recoveredNewSolution = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + + Assert.Equal(newSolutionChecksum, await recoveredNewSolution.CompilationState.GetChecksumAsync(CancellationToken.None)); + + // do same once updating primary workspace + await remoteWorkspace.UpdatePrimaryBranchSolutionAsync(assetProvider, newSolutionChecksum, CancellationToken.None); + var third = await remoteWorkspace.GetTestAccessor().GetSolutionAsync(assetProvider, newSolutionChecksum, updatePrimaryBranch: false, CancellationToken.None); + + Assert.Equal(newSolutionChecksum, await third.CompilationState.GetChecksumAsync(CancellationToken.None)); + + newSolutionValidator?.Invoke(recoveredNewSolution); + } + + private static async Task GetAssetProviderAsync(Workspace workspace, RemoteWorkspace remoteWorkspace, Solution solution, Dictionary map = null) + { + // make sure checksum is calculated + await solution.CompilationState.GetChecksumAsync(CancellationToken.None); + + map ??= []; + await solution.AppendAssetMapAsync(map, CancellationToken.None); + + var sessionId = Checksum.Create(ImmutableArray.CreateRange(Guid.NewGuid().ToByteArray())); + var storage = new SolutionAssetCache(); + var assetSource = new SimpleAssetSource(workspace.Services.GetService(), map); + + return new AssetProvider(sessionId, storage, assetSource, remoteWorkspace.Services.GetService()); } } diff --git a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs index 51c11425410a8..4eaec828a85b0 100644 --- a/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs +++ b/src/VisualStudio/Core/Test.Next/Services/VisualStudioDiagnosticAnalyzerExecutorTests.cs @@ -192,11 +192,7 @@ void Method() analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideAnalyzerOptions)); - // no result for open file only analyzer unless forced - var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: false, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); - Assert.Empty(result.AnalysisResult); - - result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: true, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); + var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]]; // check result @@ -234,7 +230,7 @@ void Method() var compilationWithAnalyzers = (await project.GetCompilationAsync()) .WithAnalyzers(analyzers, new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideAnalyzerOptions)); - var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, forceExecuteAllAnalyzers: false, + var result = await runner.AnalyzeProjectAsync(project, compilationWithAnalyzers, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken: CancellationToken.None); var analyzerResult = result.AnalysisResult[compilationWithAnalyzers.Analyzers[0]]; @@ -258,8 +254,8 @@ private static async Task AnalyzeAsync(TestWorkspace w analyzerReference.GetAnalyzers(project.Language).Where(a => a.GetType() == analyzerType).ToImmutableArray(), new WorkspaceAnalyzerOptions(project.AnalyzerOptions, ideOptions)); - var result = await executor.AnalyzeProjectAsync(project, analyzerDriver, forceExecuteAllAnalyzers: true, logPerformanceInfo: false, - getTelemetryInfo: false, cancellationToken); + var result = await executor.AnalyzeProjectAsync( + project, analyzerDriver, logPerformanceInfo: false, getTelemetryInfo: false, cancellationToken); return result.AnalysisResult[analyzerDriver.Analyzers[0]]; } diff --git a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb index 0c278d44d7ece..edbf03f93fae6 100644 --- a/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb +++ b/src/VisualStudio/Core/Test/Diagnostics/ExternalDiagnosticUpdateSourceTests.vb @@ -430,11 +430,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData)() End Function - Public Function GetDiagnosticsAsync(solution As Solution, projectId As ProjectId, documentId As DocumentId, includeSuppressedDiagnostics As Boolean, includeNonLocalDocumentDiagnostics As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsAsync - Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData)() - End Function - - Public Function GetDiagnosticsForIdsAsync(solution As Solution, projectId As ProjectId, documentId As DocumentId, diagnosticIds As ImmutableHashSet(Of String), shouldIncludeAnalyzer As Func(Of DiagnosticAnalyzer, Boolean), includeSuppressedDiagnostics As Boolean, includeLocalDocumentDiagnostics As Boolean, includeNonLocalDocumentDiagnostics As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForIdsAsync + Public Function GetDiagnosticsForIdsAsync(solution As Solution, projectId As ProjectId, documentId As DocumentId, diagnosticIds As ImmutableHashSet(Of String), shouldIncludeAnalyzer As Func(Of DiagnosticAnalyzer, Boolean), getDocuments As Func(Of Project, DocumentId, IReadOnlyList(Of DocumentId)), includeSuppressedDiagnostics As Boolean, includeLocalDocumentDiagnostics As Boolean, includeNonLocalDocumentDiagnostics As Boolean, cancellationToken As CancellationToken) As Task(Of ImmutableArray(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForIdsAsync Return SpecializedTasks.EmptyImmutableArray(Of DiagnosticData)() End Function diff --git a/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb b/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb index 0f6d62a5a75da..25ca0b50529f7 100644 --- a/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb +++ b/src/VisualStudio/Core/Test/Venus/DocumentService_IntegrationTests.vb @@ -238,9 +238,9 @@ class { } diagnosticService.CreateIncrementalAnalyzer(workspace) ' confirm that IDE doesn't report the diagnostics - Dim diagnostics = Await diagnosticService.GetDiagnosticsAsync(workspace.CurrentSolution, projectId:=Nothing, documentId:=document.Id, - includeSuppressedDiagnostics:=False, includeNonLocalDocumentDiagnostics:=True, - CancellationToken.None) + Dim diagnostics = Await diagnosticService.GetDiagnosticsForIdsAsync( + workspace.CurrentSolution, projectId:=Nothing, documentId:=document.Id, diagnosticIds:=Nothing, shouldIncludeAnalyzer:=Nothing, + includeSuppressedDiagnostics:=False, includeLocalDocumentDiagnostics:=True, includeNonLocalDocumentDiagnostics:=True, CancellationToken.None) Assert.False(diagnostics.Any()) End Using End Function diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs index 8af34dd060cae..d3da6ef64f484 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/EditorInProcess.cs @@ -309,7 +309,7 @@ public async Task GetLightBulbPreviewClassificationsAsync( activeSession.Collapse(); var classifier = classifierAggregatorService.GetClassifier(preview); var classifiedSpans = classifier.GetClassificationSpans(new SnapshotSpan(preview.TextBuffer.CurrentSnapshot, 0, preview.TextBuffer.CurrentSnapshot.Length)); - return classifiedSpans.ToArray(); + return [.. classifiedSpans]; } activeSession.Collapse(); diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs index 517c21d606ad1..c9ca70e510dc0 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InProcess/ScreenshotInProcess.cs @@ -157,7 +157,7 @@ static ScreenshotInProcess() return; } - frames = s_frames.ToArray(); + frames = [.. s_frames]; } // Make sure the frames are processed in order of their timestamps @@ -303,7 +303,7 @@ private static (TimeSpan elapsed, BitmapSource image, Size offset)[] DetectChang Marshal.FreeHGlobal(imageBuffer); } - return resultFrames.ToArray(); + return [.. resultFrames]; } private static void WritePngSignature(Stream stream, byte[] buffer) diff --git a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs index c11174300d08a..f520004406aed 100644 --- a/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs +++ b/src/VisualStudio/IntegrationTest/New.IntegrationTests/InfrastructureTests.cs @@ -22,7 +22,7 @@ public InfrastructureTests() protected override string LanguageName => LanguageNames.CSharp; - [IdeFact] + [IdeFact(Skip = "https://github.com/dotnet/roslyn/issues/73099")] public async Task CanCloseSaveDialog() { await SetUpEditorAsync( diff --git a/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs b/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs index e922257ddd994..38c1fb7179dc0 100644 --- a/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs +++ b/src/VisualStudio/LiveShare/Impl/Client/LanguageServiceUtils.cs @@ -23,7 +23,7 @@ public static string GetLanguageServerProviderServiceName(string[] contentTypes) public static string GetLanguageServerProviderServiceName(string lspServiceName) => LanguageServerProviderServiceName + "-" + lspServiceName; - public static string GetContentTypesName(string[] contentTypes) => string.Join("-", contentTypes.OrderBy(c => c).ToArray()); + public static string GetContentTypesName(string[] contentTypes) => string.Join("-", [.. contentTypes.OrderBy(c => c)]); public static bool IsContentTypeRemote(string contentType) => contentType.EndsWith("-remote"); diff --git a/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj b/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj index 5d2ff4a4c435a..d739438f4952f 100644 --- a/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj +++ b/src/VisualStudio/Setup/Roslyn.VisualStudio.Setup.csproj @@ -135,12 +135,6 @@ true BindingRedirect - - Microsoft.CommonLanguageServerProtocolFramework - BuiltProjectOutputGroup;SatelliteDllsProjectOutputGroup - true - BindingRedirect - LiveShareLanguageServices BuiltProjectOutputGroup;SatelliteDllsProjectOutputGroup diff --git a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs index 38fd06245c515..e949ef523ae1c 100644 --- a/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs +++ b/src/VisualStudio/VisualStudioDiagnosticsToolWindow/Panels/WorkspacePanel.xaml.cs @@ -99,7 +99,7 @@ async Task CompareDocumentAsync(Document document) { lock (gate) { - output.AppendLine($"{document.FilePath}: {BitConverter.ToString(snapshotChecksum.ToArray())} : {BitConverter.ToString(fileChecksum.ToArray())}"); + output.AppendLine($"{document.FilePath}: {BitConverter.ToString([.. snapshotChecksum])} : {BitConverter.ToString([.. fileChecksum])}"); outOfDateCount++; } } diff --git a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs index e7c14eb07a3eb..a1ee575ad5aed 100644 --- a/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs +++ b/src/VisualStudio/Xaml/Impl/Implementation/LanguageServer/Handler/Definitions/GoToDefinitionHandler.cs @@ -55,13 +55,13 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe var document = context.Document; if (document == null) { - return locations.ToArray(); + return [.. locations]; } var xamlGoToDefinitionService = document.Project.Services.GetService(); if (xamlGoToDefinitionService == null) { - return locations.ToArray(); + return [.. locations]; } var position = await document.GetPositionFromLinePositionAsync(ProtocolConversions.PositionToLinePosition(request.Position), cancellationToken).ConfigureAwait(false); @@ -83,7 +83,7 @@ public GoToDefinitionHandler(IMetadataAsSourceFileService metadataAsSourceFileSe await Task.WhenAll(tasks).ConfigureAwait(false); - return locations.ToArray(); + return [.. locations]; } private async Task GetLocationsAsync(XamlDefinition definition, RequestContext context, CancellationToken cancellationToken) diff --git a/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs b/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs index c45a418d8c073..5df82749fae48 100644 --- a/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs +++ b/src/Workspaces/CSharp/Portable/Formatting/CSharpSyntaxFormattingService.cs @@ -148,7 +148,7 @@ public ImmutableArray GetFormattingChangesOnTypedCharacter( return changes; } - return FormatToken(document, indentationOptions, token, formattingRules, cancellationToken).ToImmutableArray(); + return [.. FormatToken(document, indentationOptions, token, formattingRules, cancellationToken)]; } private static bool OnlySmartIndentCloseBrace(in AutoFormattingOptions options) @@ -199,7 +199,7 @@ private static ImmutableArray FormatRange( var formatter = new CSharpSmartTokenFormatter(options, formattingRules, (CompilationUnitSyntax)document.Root, document.Text); var changes = formatter.FormatRange(tokenRange.Value.Item1, tokenRange.Value.Item2, cancellationToken); - return changes.ToImmutableArray(); + return [.. changes]; } private static IEnumerable GetTypingRules(SyntaxToken tokenBeforeCaret) @@ -318,9 +318,12 @@ or SyntaxKind.EndOfDirectiveToken private ImmutableArray GetFormattingRules(ParsedDocument document, int position, SyntaxToken tokenBeforeCaret) { var formattingRuleFactory = _services.SolutionServices.GetRequiredService(); - return ImmutableArray.Create(formattingRuleFactory.CreateRule(document, position)) - .AddRange(GetTypingRules(tokenBeforeCaret)) - .AddRange(Formatter.GetDefaultFormattingRules(_services)); + return + [ + formattingRuleFactory.CreateRule(document, position), + .. GetTypingRules(tokenBeforeCaret), + .. Formatter.GetDefaultFormattingRules(_services), + ]; } public ImmutableArray GetFormattingChangesOnPaste(ParsedDocument document, TextSpan textSpan, SyntaxFormattingOptions options, CancellationToken cancellationToken) @@ -332,7 +335,7 @@ public ImmutableArray GetFormattingChangesOnPaste(ParsedDocument doc rules.AddRange(service.GetDefaultFormattingRules()); var result = service.GetFormattingResult(document.Root, [formattingSpan], options, rules, cancellationToken); - return result.GetTextChanges(cancellationToken).ToImmutableArray(); + return [.. result.GetTextChanges(cancellationToken)]; } internal sealed class PasteFormattingRule : AbstractFormattingRule diff --git a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs index 127cfd73e6a56..55bdb955984c6 100644 --- a/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs +++ b/src/Workspaces/CSharp/Portable/Simplification/CSharpSimplificationService.NodesAndTokensToReduceComputer.cs @@ -31,7 +31,7 @@ public static ImmutableArray Compute(SyntaxNode root, Func< { var reduceNodeComputer = new NodesAndTokensToReduceComputer(isNodeOrTokenOutsideSimplifySpans); reduceNodeComputer.Visit(root); - return reduceNodeComputer._nodesAndTokensToReduce.ToImmutableArray(); + return [.. reduceNodeComputer._nodesAndTokensToReduce]; } private NodesAndTokensToReduceComputer(Func isNodeOrTokenOutsideSimplifySpans) diff --git a/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs b/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs index 9a331882f074f..0cba06c47234c 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/Build/ProjectBuildManager.cs @@ -260,7 +260,7 @@ public void EndBatchBuild() _batchBuildLogger?.SetProjectAndLog(projectInstance.FullPath, log); - var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, targets.ToArray()); + var buildRequestData = new MSB.Execution.BuildRequestData(projectInstance, [.. targets]); var result = await BuildAsync(buildRequestData, cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs index ef9950a71ca1b..0dcc41acbffff 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/Extensions.cs @@ -48,7 +48,7 @@ public static ImmutableArray GetPackageReferences(this MSB.Exe references.Add(packageReference); } - return references.ToImmutableArray(); + return [.. references]; } /// diff --git a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs index 4fa6436771ed5..92fb55d25e1b1 100644 --- a/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs +++ b/src/Workspaces/Core/MSBuild.BuildHost/MSBuild/ProjectFile/ProjectFile.cs @@ -39,7 +39,7 @@ protected ProjectFile(ProjectFileLoader loader, MSB.Evaluation.Project? loadedPr Log = log; } - public ImmutableArray GetDiagnosticLogItems() => Log.ToImmutableArray(); + public ImmutableArray GetDiagnosticLogItems() => [.. Log]; protected abstract SourceCodeKind GetSourceCodeKind(string documentFileName); public abstract string GetDocumentExtension(SourceCodeKind kind); @@ -252,7 +252,7 @@ private ImmutableArray GetRelativeFolders(MSB.Framework.ITaskItem docume var linkPath = documentItem.GetMetadata(MetadataNames.Link); if (!RoslynString.IsNullOrEmpty(linkPath)) { - return PathUtilities.GetDirectoryName(linkPath).Split(PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar).ToImmutableArray(); + return [.. PathUtilities.GetDirectoryName(linkPath).Split(PathUtilities.DirectorySeparatorChar, PathUtilities.AltDirectorySeparatorChar)]; } else { diff --git a/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs b/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs index e50bf7d57852b..0bdbcd428bba0 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/BuildHostProcessManager.cs @@ -149,7 +149,7 @@ public async ValueTask DisposeAsync() // may try to mutate the list while we're enumerating. using (await _gate.DisposableWaitAsync().ConfigureAwait(false)) { - processesToDispose = _processes.Values.ToList(); + processesToDispose = [.. _processes.Values]; _processes.Clear(); } diff --git a/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs b/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs index d22ddc0d0a33e..c5c2b09fd0eb2 100644 --- a/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs +++ b/src/Workspaces/Core/MSBuild/MSBuild/MSBuildWorkspace.cs @@ -457,7 +457,7 @@ protected override void ApplyDocumentAdded(DocumentInfo info, SourceText text) var fileName = Path.ChangeExtension(info.Name, extension); var relativePath = (info.Folders != null && info.Folders.Count > 0) - ? Path.Combine(Path.Combine(info.Folders.ToArray()), fileName) + ? Path.Combine(Path.Combine([.. info.Folders]), fileName) : fileName; var fullPath = GetAbsolutePath(relativePath, Path.GetDirectoryName(project.FilePath)!); diff --git a/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs b/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs index f8178c6c5933f..a40000b3ff50b 100644 --- a/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs +++ b/src/Workspaces/Core/Portable/CaseCorrection/AbstractCaseCorrectionService.cs @@ -52,7 +52,7 @@ private SyntaxNode CaseCorrect(SemanticModel? semanticModel, SyntaxNode root, Im using (Logger.LogBlock(FunctionId.CaseCorrection_AddReplacements, cancellationToken)) { - AddReplacements(semanticModel, root, normalizedSpanCollection.ToImmutableArray(), replacements, cancellationToken); + AddReplacements(semanticModel, root, [.. normalizedSpanCollection], replacements, cancellationToken); } using (Logger.LogBlock(FunctionId.CaseCorrection_ReplaceTokens, cancellationToken)) diff --git a/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs b/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs index 1d45eb4e6ab7b..dd8e71e8f7906 100644 --- a/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs +++ b/src/Workspaces/Core/Portable/Classification/ClassifierHelper.cs @@ -139,7 +139,7 @@ private static ImmutableArray MergeClassifiedSpans( // be gaps in what it produces. Fill in those gaps so we have *all* parts of the span classified properly. using var _2 = Classifier.GetPooledList(out var filledInSpans); FillInClassifiedSpanGaps(widenedSpan.Start, mergedSpans, filledInSpans); - return filledInSpans.ToImmutableArray(); + return [.. filledInSpans]; } private static readonly Comparison s_spanComparison = static (s1, s2) => s1.TextSpan.Start - s2.TextSpan.Start; diff --git a/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs b/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs index a7bec9d9175e8..2a65deb55737d 100644 --- a/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs +++ b/src/Workspaces/Core/Portable/CodeCleanup/AbstractCodeCleanerService.cs @@ -530,7 +530,7 @@ private ImmutableArray GetSpans( // Remove the spans we should not touch from the requested spans and return that final set. var result = NormalizedTextSpanCollection.Difference(requestedSpans, spansToAvoid); - return result.ToImmutableArray(); + return [.. result]; } private async Task IterateAllCodeCleanupProvidersAsync( @@ -594,7 +594,7 @@ private string GetCodeCleanerTypeName(ICodeCleanupProvider codeCleaner) private static SyntaxNode InjectAnnotations(SyntaxNode node, Dictionary> map) { var tokenMap = map.ToDictionary(p => p.Key, p => p.Value); - return node.ReplaceTokens(tokenMap.Keys, (o, n) => o.WithAdditionalAnnotations(tokenMap[o].ToArray())); + return node.ReplaceTokens(tokenMap.Keys, (o, n) => o.WithAdditionalAnnotations([.. tokenMap[o]])); } private static bool TryCreateTextSpan(int start, int end, out TextSpan span) diff --git a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs index 680b935e03483..883c28be7be1a 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/DiagnosticAnalysisResultBuilder.cs @@ -30,11 +30,11 @@ internal struct DiagnosticAnalysisResultBuilder(Project project, VersionStamp ve private List? _lazyOthers = null; - public readonly ImmutableHashSet DocumentIds => _lazyDocumentsWithDiagnostics == null ? [] : _lazyDocumentsWithDiagnostics.ToImmutableHashSet(); + public readonly ImmutableHashSet DocumentIds => _lazyDocumentsWithDiagnostics == null ? [] : [.. _lazyDocumentsWithDiagnostics]; public readonly ImmutableDictionary> SyntaxLocals => Convert(_lazySyntaxLocals); public readonly ImmutableDictionary> SemanticLocals => Convert(_lazySemanticLocals); public readonly ImmutableDictionary> NonLocals => Convert(_lazyNonLocals); - public readonly ImmutableArray Others => _lazyOthers == null ? [] : _lazyOthers.ToImmutableArray(); + public readonly ImmutableArray Others => _lazyOthers == null ? [] : [.. _lazyOthers]; public void AddExternalSyntaxDiagnostics(DocumentId documentId, IEnumerable diagnostics) { diff --git a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs index 6128338a0a43c..8e3b0c044b186 100644 --- a/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs +++ b/src/Workspaces/Core/Portable/Diagnostics/Extensions.cs @@ -404,7 +404,7 @@ private static async Task> GetPragmaSuppressionAnalyz } await Task.WhenAll(tasks).ConfigureAwait(false); - return bag.ToImmutableArray(); + return [.. bag]; } else { diff --git a/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs b/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs index 555dfa7b6826f..6615ecbd563fe 100644 --- a/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs +++ b/src/Workspaces/Core/Portable/Editing/ImportAdderService.cs @@ -229,7 +229,7 @@ private async Task AddImportDirectivesFromSymbolAnnotationsAsync( var importContainer = addImportsService.GetImportContainer(root, context, importToSyntax.First().Value, options); // Now remove any imports we think can cause conflicts in that container. - var safeImportsToAdd = GetSafeToAddImports(importToSyntax.Keys.ToImmutableArray(), importContainer, model, cancellationToken); + var safeImportsToAdd = GetSafeToAddImports([.. importToSyntax.Keys], importContainer, model, cancellationToken); var importsToAdd = importToSyntax.Where(kvp => safeImportsToAdd.Contains(kvp.Key)).Select(kvp => kvp.Value).ToImmutableArray(); if (importsToAdd.Length == 0) diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs index 225da746a1431..380a3c3bf55f0 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentProjectsFinder.cs @@ -35,7 +35,7 @@ public static async Task> GetDependentProjectsAsync( { // namespaces are visible in all projects. if (symbols.Any(static s => s.Kind == SymbolKind.Namespace)) - return projects.ToImmutableArray(); + return [.. projects]; var dependentProjects = await GetDependentProjectsWorkerAsync(solution, symbols, cancellationToken).ConfigureAwait(false); return dependentProjects.WhereAsArray(projects.Contains); @@ -84,7 +84,7 @@ private static async Task> GetDependentProjectsWorkerAsy result.AddRange(filteredProjects.Select(p => p.project)); } - return result.ToImmutableArray(); + return [.. result]; } /// @@ -145,7 +145,7 @@ private static async Task> GetDependentProjectsWorkerAsy // further submissions can bind to them. await AddSubmissionDependentProjectsAsync(solution, symbolOrigination.sourceProject, dependentProjects, cancellationToken).ConfigureAwait(false); - return dependentProjects.ToImmutableArray(); + return [.. dependentProjects]; } private static async Task AddSubmissionDependentProjectsAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs index 79f7ef5d7893a..9e360bd3f47cb 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/DependentTypeFinder.cs @@ -136,7 +136,7 @@ private static async Task> DescendInheritanceTr await DescendInheritanceTreeInProjectAsync(project).ConfigureAwait(false); } - return result.ToImmutableArray(); + return [.. result]; async Task DescendInheritanceTreeInProjectAsync(Project project) { @@ -472,7 +472,7 @@ private static ImmutableArray OrderTopologically( index++; } - return projectsToExamine.OrderBy((p1, p2) => order[p1.Id] - order[p2.Id]).ToImmutableArray(); + return [.. projectsToExamine.OrderBy((p1, p2) => order[p1.Id] - order[p2.Id])]; } private static ImmutableArray GetProjectsToExamineWorker( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs index 05efca6bffc64..aaad258c3e4e3 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferenceCache.cs @@ -17,14 +17,32 @@ namespace Microsoft.CodeAnalysis.FindSymbols; +/// +/// Caches information find-references needs associated with each document. Computed and cached so that multiple calls +/// to find-references in a row can share the same data. +/// internal sealed class FindReferenceCache { - private static readonly ConditionalWeakTable s_cache = new(); + private static readonly ConditionalWeakTable> s_cache = new(); - public static FindReferenceCache GetCache(SemanticModel model) - => s_cache.GetValue(model, static model => new(model)); + public static async ValueTask GetCacheAsync(Document document, CancellationToken cancellationToken) + { + var lazy = s_cache.GetValue(document, static document => AsyncLazy.Create(ComputeCacheAsync, document)); + return await lazy.GetValueAsync(cancellationToken).ConfigureAwait(false); - private readonly SemanticModel _semanticModel; + static async Task ComputeCacheAsync(Document document, CancellationToken cancellationToken) + { + // Find-Refs is not impacted by nullable types at all. So get a nullable-disabled semantic model to avoid + // unnecessary costs while binding. + var model = await document.GetRequiredNullableDisabledSemanticModelAsync(cancellationToken).ConfigureAwait(false); + var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + return new(document, model, root); + } + } + + public readonly Document Document; + public readonly SemanticModel SemanticModel; + public readonly SyntaxNode Root; private readonly ConcurrentDictionary _symbolInfoCache = []; private readonly ConcurrentDictionary> _identifierCache; @@ -32,9 +50,11 @@ public static FindReferenceCache GetCache(SemanticModel model) private ImmutableHashSet? _aliasNameSet; private ImmutableArray _constructorInitializerCache; - private FindReferenceCache(SemanticModel semanticModel) + private FindReferenceCache(Document document, SemanticModel semanticModel, SyntaxNode root) { - _semanticModel = semanticModel; + Document = document; + SemanticModel = semanticModel; + Root = root; _identifierCache = new(comparer: semanticModel.Language switch { LanguageNames.VisualBasic => StringComparer.OrdinalIgnoreCase, @@ -44,21 +64,19 @@ private FindReferenceCache(SemanticModel semanticModel) } public SymbolInfo GetSymbolInfo(SyntaxNode node, CancellationToken cancellationToken) - { - return _symbolInfoCache.GetOrAdd(node, static (n, arg) => arg._semanticModel.GetSymbolInfo(n, arg.cancellationToken), (_semanticModel, cancellationToken)); - } + => _symbolInfoCache.GetOrAdd(node, static (n, arg) => arg.SemanticModel.GetSymbolInfo(n, arg.cancellationToken), (SemanticModel, cancellationToken)); public IAliasSymbol? GetAliasInfo( ISemanticFactsService semanticFacts, SyntaxToken token, CancellationToken cancellationToken) { if (_aliasNameSet == null) { - var set = semanticFacts.GetAliasNameSet(_semanticModel, cancellationToken); + var set = semanticFacts.GetAliasNameSet(SemanticModel, cancellationToken); Interlocked.CompareExchange(ref _aliasNameSet, set, null); } if (_aliasNameSet.Contains(token.ValueText)) - return _semanticModel.GetAliasInfo(token.GetRequiredParent(), cancellationToken); + return SemanticModel.GetAliasInfo(token.GetRequiredParent(), cancellationToken); return null; } @@ -95,7 +113,7 @@ static async ValueTask> ComputeAndCacheTokensAsync( FindReferenceCache cache, Document document, string identifier, SyntaxTreeIndex info, CancellationToken cancellationToken) { var syntaxFacts = document.GetRequiredLanguageService(); - var root = await cache._semanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); + var root = await cache.SemanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); // If the identifier was escaped in the file then we'll have to do a more involved search that actually // walks the root and checks all identifier tokens. diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesDocumentState.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesDocumentState.cs index 59d513ef3bc73..7a2fb1f8c7d6a 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesDocumentState.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesDocumentState.cs @@ -8,23 +8,25 @@ namespace Microsoft.CodeAnalysis.FindSymbols; -internal class FindReferencesDocumentState( - Document document, - SemanticModel semanticModel, - SyntaxNode root, +/// +/// Ephemeral information that find-references needs for a particular document when searching for a specific +/// symbol. Importantly, it contains the global aliases to that symbol within the current project. +/// +internal sealed class FindReferencesDocumentState( FindReferenceCache cache, HashSet? globalAliases) { private static readonly HashSet s_empty = []; - public readonly Document Document = document; - public readonly SemanticModel SemanticModel = semanticModel; - public readonly SyntaxNode Root = root; public readonly FindReferenceCache Cache = cache; public readonly HashSet GlobalAliases = globalAliases ?? s_empty; - public readonly Solution Solution = document.Project.Solution; - public readonly SyntaxTree SyntaxTree = semanticModel.SyntaxTree; - public readonly ISyntaxFactsService SyntaxFacts = document.GetRequiredLanguageService(); - public readonly ISemanticFactsService SemanticFacts = document.GetRequiredLanguageService(); + public Document Document => this.Cache.Document; + public SyntaxNode Root => this.Cache.Root; + public SemanticModel SemanticModel => this.Cache.SemanticModel; + public SyntaxTree SyntaxTree => this.SemanticModel.SyntaxTree; + + public Solution Solution => this.Document.Project.Solution; + public ISyntaxFactsService SyntaxFacts => this.Document.GetRequiredLanguageService(); + public ISemanticFactsService SemanticFacts => this.Document.GetRequiredLanguageService(); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs index c877f21a1ee4f..cf11c3110e238 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.BidirectionalSymbolSet.cs @@ -42,7 +42,7 @@ public BidirectionalSymbolSet( } public override ImmutableArray GetAllSymbols() - => _allSymbols.ToImmutableArray(); + => [.. _allSymbols]; public override async Task InheritanceCascadeAsync(Project project, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs index a6778eb0608a7..2463a1d7033dc 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.NonCascadingSymbolSet.cs @@ -16,7 +16,7 @@ internal partial class FindReferencesSearchEngine /// private sealed class NonCascadingSymbolSet(FindReferencesSearchEngine engine, MetadataUnifyingSymbolHashSet searchSymbols) : SymbolSet(engine) { - private readonly ImmutableArray _symbols = searchSymbols.ToImmutableArray(); + private readonly ImmutableArray _symbols = [.. searchSymbols]; public override ImmutableArray GetAllSymbols() => _symbols; diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs index 9e87a98d6648e..9e15a2d7db461 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.UnidirectionalSymbolSet.cs @@ -40,7 +40,7 @@ public override ImmutableArray GetAllSymbols() var result = new MetadataUnifyingSymbolHashSet(); result.AddRange(_upSymbols); result.AddRange(initialSymbols); - return result.ToImmutableArray(); + return [.. result]; } public override async Task InheritanceCascadeAsync(Project project, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs index b29348e91c29c..eb3a7e932959e 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine.cs @@ -190,6 +190,9 @@ private async Task ProcessProjectAsync(Project project, ImmutableArray using var _2 = PooledDictionary.GetInstance(out var documentToSymbols); try { + // scratch hashset to place results in. Populated/inspected/cleared in inner loop. + using var _3 = PooledHashSet.GetInstance(out var foundDocuments); + await AddGlobalAliasesAsync(project, allSymbols, symbolToGlobalAliases, cancellationToken).ConfigureAwait(false); foreach (var symbol in allSymbols) @@ -198,18 +201,23 @@ private async Task ProcessProjectAsync(Project project, ImmutableArray foreach (var finder in _finders) { - var documents = await finder.DetermineDocumentsToSearchAsync( - symbol, globalAliases, project, _documents, _options, cancellationToken).ConfigureAwait(false); + await finder.DetermineDocumentsToSearchAsync( + symbol, globalAliases, project, _documents, + StandardCallbacks.AddToHashSet, + foundDocuments, + _options, cancellationToken).ConfigureAwait(false); - foreach (var document in documents) + foreach (var document in foundDocuments) { var docSymbols = GetSymbolSet(documentToSymbols, document); docSymbols.Add(symbol); } + + foundDocuments.Clear(); } } - using var _3 = ArrayBuilder.GetInstance(out var tasks); + using var _4 = ArrayBuilder.GetInstance(out var tasks); foreach (var (document, docSymbols) in documentToSymbols) { tasks.Add(CreateWorkAsync(() => ProcessDocumentAsync( @@ -262,16 +270,17 @@ private async Task ProcessDocumentAsync( // the symbol being searched for). As such, we're almost certainly going to have to do semantic checks // to now see if the candidate actually matches the symbol. This will require syntax and semantics. So // just grab those once here and hold onto them for the lifetime of this call. - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var cache = FindReferenceCache.GetCache(model); + var cache = await FindReferenceCache.GetCacheAsync(document, cancellationToken).ConfigureAwait(false); + + // scratch array to place results in. Populated/inspected/cleared in inner loop. + using var _ = ArrayBuilder.GetInstance(out var foundReferenceLocations); foreach (var symbol in symbols) { var globalAliases = TryGet(symbolToGlobalAliases, symbol); - var state = new FindReferencesDocumentState( - document, model, root, cache, globalAliases); - await ProcessDocumentAsync(symbol, state).ConfigureAwait(false); + var state = new FindReferencesDocumentState(cache, globalAliases); + + await ProcessDocumentAsync(symbol, state, foundReferenceLocations).ConfigureAwait(false); } } finally @@ -280,8 +289,7 @@ private async Task ProcessDocumentAsync( } async Task ProcessDocumentAsync( - ISymbol symbol, - FindReferencesDocumentState state) + ISymbol symbol, FindReferencesDocumentState state, ArrayBuilder foundReferenceLocations) { cancellationToken.ThrowIfCancellationRequested(); @@ -292,10 +300,12 @@ async Task ProcessDocumentAsync( var group = _symbolToGroup[symbol]; foreach (var finder in _finders) { - var references = await finder.FindReferencesInDocumentAsync( - symbol, state, _options, cancellationToken).ConfigureAwait(false); - foreach (var (_, location) in references) + await finder.FindReferencesInDocumentAsync( + symbol, state, StandardCallbacks.AddToArrayBuilder, foundReferenceLocations, _options, cancellationToken).ConfigureAwait(false); + foreach (var (_, location) in foundReferenceLocations) await _progress.OnReferenceFoundAsync(group, symbol, location, cancellationToken).ConfigureAwait(false); + + foundReferenceLocations.Clear(); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs index 6c5dc334823e7..cb31339e8022c 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/FindReferencesSearchEngine_FindReferencesInDocuments.cs @@ -87,32 +87,35 @@ async ValueTask PerformSearchInDocumentAsync( // appropriate finders checking this document for hits. We're likely going to need to perform syntax // and semantics checks in this file. So just grab those once here and hold onto them for the lifetime // of this call. - var model = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false); - var root = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false); - var cache = FindReferenceCache.GetCache(model); + var cache = await FindReferenceCache.GetCacheAsync(document, cancellationToken).ConfigureAwait(false); foreach (var symbol in symbols) { - var globalAliases = GetGlobalAliasesSet(symbolToGlobalAliases, symbol); - var state = new FindReferencesDocumentState(document, model, root, cache, globalAliases); + var globalAliases = TryGet(symbolToGlobalAliases, symbol); + var state = new FindReferencesDocumentState(cache, globalAliases); - await PerformSearchInDocumentWorkerAsync(symbol, document, state).ConfigureAwait(false); + await PerformSearchInDocumentWorkerAsync(symbol, state).ConfigureAwait(false); } } async ValueTask PerformSearchInDocumentWorkerAsync( - ISymbol symbol, Document document, FindReferencesDocumentState state) + ISymbol symbol, FindReferencesDocumentState state) { + // Scratch buffer to place references for each finder. Cleared at the end of every loop iteration. + using var _ = ArrayBuilder.GetInstance(out var referencesForFinder); + // Always perform a normal search, looking for direct references to exactly that symbol. foreach (var finder in _finders) { - var references = await finder.FindReferencesInDocumentAsync( - symbol, state, _options, cancellationToken).ConfigureAwait(false); - foreach (var (_, location) in references) + await finder.FindReferencesInDocumentAsync( + symbol, state, StandardCallbacks.AddToArrayBuilder, referencesForFinder, _options, cancellationToken).ConfigureAwait(false); + foreach (var (_, location) in referencesForFinder) { var group = await ReportGroupAsync(symbol, cancellationToken).ConfigureAwait(false); await _progress.OnReferenceFoundAsync(group, symbol, location, cancellationToken).ConfigureAwait(false); } + + referencesForFinder.Clear(); } // Now, for symbols that could involve inheritance, look for references to the same named entity, and diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.cs index 224ab915fc901..574d7b6652cd9 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMemberScopedReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Linq; @@ -23,45 +24,49 @@ protected abstract bool TokensMatch( protected sealed override bool CanFind(TSymbol symbol) => true; - protected sealed override Task> DetermineDocumentsToSearchAsync( + protected sealed override Task DetermineDocumentsToSearchAsync( TSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var location = symbol.Locations.FirstOrDefault(); if (location == null || !location.IsInSource) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; var document = project.GetDocument(location.SourceTree); if (document == null) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; if (documents != null && !documents.Contains(document)) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; - return Task.FromResult(ImmutableArray.Create(document)); + processResult(document, processResultData); + return Task.CompletedTask; } - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override async ValueTask FindReferencesInDocumentAsync( TSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var container = GetContainer(symbol); if (container != null) - return await FindReferencesInContainerAsync(symbol, container, state, cancellationToken).ConfigureAwait(false); - - if (symbol.ContainingType != null && symbol.ContainingType.IsScriptClass) + { + await FindReferencesInContainerAsync(symbol, container, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); + } + else if (symbol.ContainingType != null && symbol.ContainingType.IsScriptClass) { var tokens = await FindMatchingIdentifierTokensAsync(state, symbol.Name, cancellationToken).ConfigureAwait(false); - return await FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken).ConfigureAwait(false); + await FindReferencesInTokensAsync(symbol, state, tokens, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - - return []; } private static ISymbol? GetContainer(ISymbol symbol) @@ -92,10 +97,12 @@ protected sealed override async ValueTask> FindRe return null; } - private ValueTask> FindReferencesInContainerAsync( + private ValueTask FindReferencesInContainerAsync( TSymbol symbol, ISymbol container, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var service = state.Document.GetRequiredLanguageService(); @@ -114,6 +121,6 @@ private ValueTask> FindReferencesInContainerAsync } } - return FindReferencesInTokensAsync(symbol, state, tokens.ToImmutable(), cancellationToken); + return FindReferencesInTokensAsync(symbol, state, tokens.ToImmutable(), processResult, processResultData, cancellationToken); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs index 094c5db7de444..e9944e7ff30c0 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractMethodOrPropertyOrEventSymbolReferenceFinder.cs @@ -4,10 +4,8 @@ using System.Collections.Immutable; using System.Threading; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.PooledObjects; -using Microsoft.CodeAnalysis.Shared.Extensions; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs index 1ac57d9d93596..39b19e605551a 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder.cs @@ -30,11 +30,11 @@ public abstract Task> DetermineGlobalAliasesAsync( public abstract ValueTask> DetermineCascadedSymbolsAsync( ISymbol symbol, Solution solution, FindReferencesSearchOptions options, CancellationToken cancellationToken); - public abstract Task> DetermineDocumentsToSearchAsync( - ISymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, FindReferencesSearchOptions options, CancellationToken cancellationToken); + public abstract Task DetermineDocumentsToSearchAsync( + ISymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); - public abstract ValueTask> FindReferencesInDocumentAsync( - ISymbol symbol, FindReferencesDocumentState state, FindReferencesSearchOptions options, CancellationToken cancellationToken); + public abstract ValueTask FindReferencesInDocumentAsync( + ISymbol symbol, FindReferencesDocumentState state, Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); private static ValueTask<(bool matched, CandidateReason reason)> SymbolsMatchAsync( ISymbol symbol, FindReferencesDocumentState state, SyntaxToken token, CancellationToken cancellationToken) @@ -81,11 +81,13 @@ protected static bool TryGetNameWithoutAttributeSuffix( return name.TryGetWithoutAttributeSuffix(syntaxFacts.IsCaseSensitive, out result); } - protected static async Task> FindDocumentsAsync( + protected static async Task FindDocumentsAsync( Project project, IImmutableSet? scope, Func> predicateAsync, T value, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { // special case for highlight references @@ -93,31 +95,30 @@ protected static async Task> FindDocumentsAsync( { var document = scope.First(); if (document.Project == project) - return scope.ToImmutableArray(); + processResult(document, processResultData); - return []; + return; } - using var _ = ArrayBuilder.GetInstance(out var documents); foreach (var document in await project.GetAllRegularAndSourceGeneratedDocumentsAsync(cancellationToken).ConfigureAwait(false)) { if (scope != null && !scope.Contains(document)) continue; if (await predicateAsync(document, value, cancellationToken).ConfigureAwait(false)) - documents.Add(document); + processResult(document, processResultData); } - - return documents.ToImmutableAndClear(); } /// /// Finds all the documents in the provided project that contain the requested string /// values /// - protected static Task> FindDocumentsAsync( + protected static Task FindDocumentsAsync( Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, CancellationToken cancellationToken, params string[] values) { @@ -130,59 +131,64 @@ protected static Task> FindDocumentsAsync( } return true; - }, values, cancellationToken); + }, values, processResult, processResultData, cancellationToken); } /// /// Finds all the documents in the provided project that contain a global attribute in them. /// - protected static Task> FindDocumentsWithGlobalSuppressMessageAttributeAsync( - Project project, IImmutableSet? documents, CancellationToken cancellationToken) + protected static Task FindDocumentsWithGlobalSuppressMessageAttributeAsync( + Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( - project, documents, static index => index.ContainsGlobalSuppressMessageAttribute, cancellationToken); + project, documents, static index => index.ContainsGlobalSuppressMessageAttribute, processResult, processResultData, cancellationToken); } - protected static Task> FindDocumentsAsync( + protected static Task FindDocumentsAsync( Project project, IImmutableSet? documents, PredefinedType predefinedType, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (predefinedType == PredefinedType.None) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; return FindDocumentsWithPredicateAsync( - project, documents, static (index, predefinedType) => index.ContainsPredefinedType(predefinedType), predefinedType, cancellationToken); + project, documents, static (index, predefinedType) => index.ContainsPredefinedType(predefinedType), predefinedType, processResult, processResultData, cancellationToken); } protected static bool IdentifiersMatch(ISyntaxFactsService syntaxFacts, string name, SyntaxToken token) => syntaxFacts.IsIdentifier(token) && syntaxFacts.TextMatch(token.ValueText, name); [PerformanceSensitive("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1224834", OftenCompletesSynchronously = true)] - protected static async ValueTask> FindReferencesInDocumentUsingIdentifierAsync( + protected static async ValueTask FindReferencesInDocumentUsingIdentifierAsync( ISymbol symbol, string identifier, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var tokens = await FindMatchingIdentifierTokensAsync(state, identifier, cancellationToken).ConfigureAwait(false); - return await FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken).ConfigureAwait(false); + await FindReferencesInTokensAsync(symbol, state, tokens, processResult, processResultData, cancellationToken).ConfigureAwait(false); } public static ValueTask> FindMatchingIdentifierTokensAsync(FindReferencesDocumentState state, string identifier, CancellationToken cancellationToken) => state.Cache.FindMatchingIdentifierTokensAsync(state.Document, identifier, cancellationToken); - protected static async ValueTask> FindReferencesInTokensAsync( + protected static async ValueTask FindReferencesInTokensAsync( ISymbol symbol, FindReferencesDocumentState state, ImmutableArray tokens, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (tokens.IsEmpty) - return []; + return; - using var _ = ArrayBuilder.GetInstance(out var locations); foreach (var token in tokens) { cancellationToken.ThrowIfCancellationRequested(); @@ -193,11 +199,9 @@ protected static async ValueTask> FindReferencesI { var finderLocation = CreateFinderLocation(state, token, reason, cancellationToken); - locations.Add(finderLocation); + processResult(finderLocation, processResultData); } } - - return locations.ToImmutableAndClear(); } protected static FinderLocation CreateFinderLocation(FindReferencesDocumentState state, SyntaxToken token, CandidateReason reason, CancellationToken cancellationToken) @@ -239,27 +243,29 @@ public static ReferenceLocation CreateReferenceLocation(FindReferencesDocumentSt return null; } - protected static async Task> FindLocalAliasReferencesAsync( + protected static async Task FindLocalAliasReferencesAsync( ArrayBuilder initialReferences, ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var aliasSymbols = GetLocalAliasSymbols(state, initialReferences, cancellationToken); - return aliasSymbols.IsDefaultOrEmpty - ? [] - : await FindReferencesThroughLocalAliasSymbolsAsync(symbol, state, aliasSymbols, cancellationToken).ConfigureAwait(false); + if (!aliasSymbols.IsDefaultOrEmpty) + await FindReferencesThroughLocalAliasSymbolsAsync(symbol, state, aliasSymbols, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected static async Task> FindLocalAliasReferencesAsync( + protected static async Task FindLocalAliasReferencesAsync( ArrayBuilder initialReferences, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var aliasSymbols = GetLocalAliasSymbols(state, initialReferences, cancellationToken); - return aliasSymbols.IsDefaultOrEmpty - ? [] - : await FindReferencesThroughLocalAliasSymbolsAsync(state, aliasSymbols, cancellationToken).ConfigureAwait(false); + if (!aliasSymbols.IsDefaultOrEmpty) + await FindReferencesThroughLocalAliasSymbolsAsync(state, aliasSymbols, processResult, processResultData, cancellationToken).ConfigureAwait(false); } private static ImmutableArray GetLocalAliasSymbols( @@ -278,127 +284,127 @@ private static ImmutableArray GetLocalAliasSymbols( return aliasSymbols.ToImmutableAndClear(); } - private static async Task> FindReferencesThroughLocalAliasSymbolsAsync( + private static async Task FindReferencesThroughLocalAliasSymbolsAsync( ISymbol symbol, FindReferencesDocumentState state, ImmutableArray localAliasSymbols, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var allAliasReferences); foreach (var localAliasSymbol in localAliasSymbols) { - var aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - symbol, localAliasSymbol.Name, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + await FindReferencesInDocumentUsingIdentifierAsync( + symbol, localAliasSymbol.Name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); + // the alias may reference an attribute and the alias name may end with an "Attribute" suffix. In this case search for the // shortened name as well (e.g. using GooAttribute = MyNamespace.GooAttribute; [Goo] class C1 {}) if (TryGetNameWithoutAttributeSuffix(localAliasSymbol.Name, state.SyntaxFacts, out var simpleName)) { - aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - symbol, simpleName, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + await FindReferencesInDocumentUsingIdentifierAsync( + symbol, simpleName, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } - - return allAliasReferences.ToImmutableAndClear(); } - private static async Task> FindReferencesThroughLocalAliasSymbolsAsync( + private static async Task FindReferencesThroughLocalAliasSymbolsAsync( FindReferencesDocumentState state, ImmutableArray localAliasSymbols, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var allAliasReferences); foreach (var aliasSymbol in localAliasSymbols) { - var aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - aliasSymbol, aliasSymbol.Name, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + await FindReferencesInDocumentUsingIdentifierAsync( + aliasSymbol, aliasSymbol.Name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); + // the alias may reference an attribute and the alias name may end with an "Attribute" suffix. In this case search for the // shortened name as well (e.g. using GooAttribute = MyNamespace.GooAttribute; [Goo] class C1 {}) if (TryGetNameWithoutAttributeSuffix(aliasSymbol.Name, state.SyntaxFacts, out var simpleName)) { - aliasReferences = await FindReferencesInDocumentUsingIdentifierAsync( - aliasSymbol, simpleName, state, cancellationToken).ConfigureAwait(false); - allAliasReferences.AddRange(aliasReferences); + await FindReferencesInDocumentUsingIdentifierAsync( + aliasSymbol, simpleName, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } - - return allAliasReferences.ToImmutableAndClear(); } - protected static Task> FindDocumentsWithPredicateAsync( + protected static Task FindDocumentsWithPredicateAsync( Project project, IImmutableSet? documents, Func predicate, T value, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return FindDocumentsAsync(project, documents, static async (d, t, c) => { var info = await SyntaxTreeIndex.GetRequiredIndexAsync(d, c).ConfigureAwait(false); return t.predicate(info, t.value); - }, (predicate, value), cancellationToken); + }, (predicate, value), processResult, processResultData, cancellationToken); } - protected static Task> FindDocumentsWithPredicateAsync( + protected static Task FindDocumentsWithPredicateAsync( Project project, IImmutableSet? documents, Func predicate, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( project, documents, static (info, predicate) => predicate(info), predicate, + processResult, + processResultData, cancellationToken); } - protected static Task> FindDocumentsWithForEachStatementsAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsForEachStatement, cancellationToken); + protected static Task FindDocumentsWithForEachStatementsAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsForEachStatement, processResult, processResultData, cancellationToken); /// /// If the `node` implicitly matches the `symbol`, then it will be added to `locations`. /// - protected delegate void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations); + protected delegate void CollectMatchingReferences( + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData); - protected static async Task> FindReferencesInDocumentAsync( + protected static async Task FindReferencesInDocumentAsync( FindReferencesDocumentState state, Func isRelevantDocument, - CollectMatchingReferences collectMatchingReferences, + CollectMatchingReferences collectMatchingReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var document = state.Document; var syntaxTreeInfo = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); if (isRelevantDocument(syntaxTreeInfo)) { - using var _ = ArrayBuilder.GetInstance(out var locations); - foreach (var node in state.Root.DescendantNodesAndSelf()) { cancellationToken.ThrowIfCancellationRequested(); - collectMatchingReferences(node, state, locations); + collectMatchingReferences(node, state, processResult, processResultData); } - - return locations.ToImmutableAndClear(); } - - return []; } - protected Task> FindReferencesInForEachStatementsAsync( + protected Task FindReferencesInForEachStatementsAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsForEachStatement; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var info = state.SemanticFacts.GetForEachSymbols(state.SemanticModel, node); @@ -410,30 +416,33 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location: location, isImplicit: true, symbolUsageInfo, GetAdditionalFindUsagesProperties(node, state), - candidateReason: CandidateReason.None))); + candidateReason: CandidateReason.None)); + processResult(result, processResultData); } } } - protected Task> FindReferencesInCollectionInitializerAsync( + protected Task FindReferencesInCollectionInitializerAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsCollectionInitializer; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { if (!state.SyntaxFacts.IsObjectCollectionInitializer(node)) return; @@ -448,31 +457,34 @@ void CollectMatchingReferences( var location = expression.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(expression, state, cancellationToken); - locations.Add(new FinderLocation(expression, new ReferenceLocation( + var result = new FinderLocation(expression, new ReferenceLocation( state.Document, alias: null, location: location, isImplicit: true, symbolUsageInfo, GetAdditionalFindUsagesProperties(expression, state), - candidateReason: CandidateReason.None))); + candidateReason: CandidateReason.None)); + processResult(result, processResultData); } } } } - protected Task> FindReferencesInDeconstructionAsync( + protected Task FindReferencesInDeconstructionAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsDeconstruction; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var semanticModel = state.SemanticModel; var semanticFacts = state.SemanticFacts; @@ -488,25 +500,28 @@ void CollectMatchingReferences( var location = state.SyntaxFacts.GetDeconstructionReferenceLocation(node); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } - protected Task> FindReferencesInAwaitExpressionAsync( + protected Task FindReferencesInAwaitExpressionAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsAwait; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var awaitExpressionMethod = state.SemanticFacts.GetGetAwaiterMethod(state.SemanticModel, node); @@ -515,25 +530,28 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } - protected Task> FindReferencesInImplicitObjectCreationExpressionAsync( + protected Task FindReferencesInImplicitObjectCreationExpressionAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsImplicitObjectCreation; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { // Avoid binding unrelated nodes if (!state.SyntaxFacts.IsImplicitObjectCreationExpression(node)) @@ -546,9 +564,10 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } @@ -824,12 +843,15 @@ internal abstract partial class AbstractReferenceFinder : AbstractRefer { protected abstract bool CanFind(TSymbol symbol); - protected abstract Task> DetermineDocumentsToSearchAsync( + protected abstract Task DetermineDocumentsToSearchAsync( TSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); - protected abstract ValueTask> FindReferencesInDocumentAsync( - TSymbol symbol, FindReferencesDocumentState state, FindReferencesSearchOptions options, CancellationToken cancellationToken); + protected abstract ValueTask FindReferencesInDocumentAsync( + TSymbol symbol, FindReferencesDocumentState state, + Action processResult, TData processResultData, + FindReferencesSearchOptions options, CancellationToken cancellationToken); protected virtual Task> DetermineGlobalAliasesAsync( TSymbol symbol, Project project, CancellationToken cancellationToken) @@ -845,21 +867,23 @@ public sealed override Task> DetermineGlobalAliasesAsync( : SpecializedTasks.EmptyImmutableArray(); } - public sealed override Task> DetermineDocumentsToSearchAsync( + public sealed override Task DetermineDocumentsToSearchAsync( ISymbol symbol, HashSet? globalAliases, Project project, - IImmutableSet? documents, FindReferencesSearchOptions options, CancellationToken cancellationToken) + IImmutableSet? documents, Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return symbol is TSymbol typedSymbol && CanFind(typedSymbol) - ? DetermineDocumentsToSearchAsync(typedSymbol, globalAliases, project, documents, options, cancellationToken) - : SpecializedTasks.EmptyImmutableArray(); + if (symbol is TSymbol typedSymbol && CanFind(typedSymbol)) + return DetermineDocumentsToSearchAsync(typedSymbol, globalAliases, project, documents, processResult, processResultData, options, cancellationToken); + + return Task.CompletedTask; } - public sealed override ValueTask> FindReferencesInDocumentAsync( - ISymbol symbol, FindReferencesDocumentState state, FindReferencesSearchOptions options, CancellationToken cancellationToken) + public sealed override ValueTask FindReferencesInDocumentAsync( + ISymbol symbol, FindReferencesDocumentState state, Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { return symbol is TSymbol typedSymbol && CanFind(typedSymbol) - ? FindReferencesInDocumentAsync(typedSymbol, state, options, cancellationToken) - : new ValueTask>([]); + ? FindReferencesInDocumentAsync(typedSymbol, state, processResult, processResultData, options, cancellationToken) + : ValueTaskFactory.CompletedTask; } public sealed override ValueTask> DetermineCascadedSymbolsAsync( @@ -881,11 +905,11 @@ protected virtual ValueTask> DetermineCascadedSymbolsAsy return new([]); } - protected static ValueTask> FindReferencesInDocumentUsingSymbolNameAsync( - TSymbol symbol, FindReferencesDocumentState state, CancellationToken cancellationToken) + protected static ValueTask FindReferencesInDocumentUsingSymbolNameAsync( + TSymbol symbol, FindReferencesDocumentState state, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindReferencesInDocumentUsingIdentifierAsync( - symbol, symbol.Name, state, cancellationToken); + symbol, symbol.Name, state, processResult, processResultData, cancellationToken); } protected static async Task> GetAllMatchingGlobalAliasNamesAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs index d3c9275698895..13e1de1396ca0 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractReferenceFinder_GlobalSuppressions.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; @@ -51,28 +50,30 @@ static bool SupportsGlobalSuppression(ISymbol symbol) /// [assembly: SuppressMessage("RuleCategory", "RuleId', Scope = "member", Target = "~F:C.Field")] /// [PerformanceSensitive("https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1224834", OftenCompletesSynchronously = true)] - protected static async ValueTask> FindReferencesInDocumentInsideGlobalSuppressionsAsync( + protected static async ValueTask FindReferencesInDocumentInsideGlobalSuppressionsAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (!ShouldFindReferencesInGlobalSuppressions(symbol, out var docCommentId)) - return []; + return; // Check if we have any relevant global attributes in this document. var info = await SyntaxTreeIndex.GetRequiredIndexAsync(state.Document, cancellationToken).ConfigureAwait(false); if (!info.ContainsGlobalSuppressMessageAttribute) - return []; + return; var semanticModel = state.SemanticModel; var suppressMessageAttribute = semanticModel.Compilation.SuppressMessageAttributeType(); if (suppressMessageAttribute == null) - return []; + return; // Check if we have any instances of the symbol documentation comment ID string literals within global attributes. // These string literals represent references to the symbol. if (!TryGetExpectedDocumentationCommentId(docCommentId, out var expectedDocCommentId)) - return []; + return; var syntaxFacts = state.SyntaxFacts; @@ -80,17 +81,16 @@ protected static async ValueTask> FindReferencesI // perform semantic checks to ensure these are valid references to the symbol // and if so, add these locations to the computed references. var root = await semanticModel.SyntaxTree.GetRootAsync(cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var locations); foreach (var token in root.DescendantTokens()) { if (IsCandidate(state, token, expectedDocCommentId.Span, suppressMessageAttribute, cancellationToken, out var offsetOfReferenceInToken)) { var referenceLocation = CreateReferenceLocation(offsetOfReferenceInToken, token, root, state.Document, syntaxFacts); - locations.Add(new FinderLocation(token.GetRequiredParent(), referenceLocation)); + processResult(new FinderLocation(token.GetRequiredParent(), referenceLocation), processResultData); } } - return locations.ToImmutableAndClear(); + return; // Local functions static bool IsCandidate( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs index 756ce8036d1fa..38cc211cfb9ad 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/AbstractTypeParameterSymbolReferenceFinder.cs @@ -7,16 +7,17 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; internal abstract class AbstractTypeParameterSymbolReferenceFinder : AbstractReferenceFinder { - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override async ValueTask FindReferencesInDocumentAsync( ITypeParameterSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -30,15 +31,19 @@ protected sealed override async ValueTask> FindRe var tokens = await FindMatchingIdentifierTokensAsync(state, symbol.Name, cancellationToken).ConfigureAwait(false); - var normalReferences = await FindReferencesInTokensAsync( + await FindReferencesInTokensAsync( symbol, state, tokens.WhereAsArray(static (token, state) => !IsObjectCreationToken(token, state), state), + processResult, + processResultData, cancellationToken).ConfigureAwait(false); - var objectCreationReferences = GetObjectCreationReferences( - tokens.WhereAsArray(static (token, state) => IsObjectCreationToken(token, state), state)); + GetObjectCreationReferences( + tokens.WhereAsArray(static (token, state) => IsObjectCreationToken(token, state), state), + processResult, + processResultData); - return normalReferences.Concat(objectCreationReferences); + return; static bool IsObjectCreationToken(SyntaxToken token, FindReferencesDocumentState state) { @@ -47,19 +52,18 @@ static bool IsObjectCreationToken(SyntaxToken token, FindReferencesDocumentState syntaxFacts.IsObjectCreationExpression(token.Parent.Parent); } - ImmutableArray GetObjectCreationReferences(ImmutableArray objectCreationTokens) + void GetObjectCreationReferences( + ImmutableArray objectCreationTokens, + Action processResult, + TData processResultData) { - using var _ = ArrayBuilder.GetInstance(out var result); - foreach (var token in objectCreationTokens) { Contract.ThrowIfNull(token.Parent?.Parent); var typeInfo = state.SemanticModel.GetTypeInfo(token.Parent.Parent, cancellationToken); if (symbol.Equals(typeInfo.Type, SymbolEqualityComparer.Default)) - result.Add(CreateFinderLocation(state, token, CandidateReason.None, cancellationToken)); + processResult(CreateFinderLocation(state, token, CandidateReason.None, cancellationToken), processResultData); } - - return result.ToImmutableAndClear(); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs index 88dc9fad96af2..4258a0eca666b 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorInitializerSymbolReferenceFinder.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. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Diagnostics; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -19,11 +18,13 @@ internal sealed class ConstructorInitializerSymbolReferenceFinder : AbstractRefe protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind == MethodKind.Constructor; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -47,12 +48,14 @@ protected override Task> DetermineDocumentsToSearchAsyn } return false; - }, symbol.ContainingType.Name, cancellationToken); + }, symbol.ContainingType.Name, processResult, processResultData, cancellationToken); } - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -67,7 +70,9 @@ protected sealed override async ValueTask> FindRe static (token, tuple) => TokensMatch(tuple.state, token, tuple.methodSymbol.ContainingType.Name, tuple.cancellationToken), (state, methodSymbol, cancellationToken)); - return await FindReferencesInTokensAsync(methodSymbol, state, totalTokens, cancellationToken).ConfigureAwait(false); + await FindReferencesInTokensAsync(methodSymbol, state, totalTokens, processResult, processResultData, cancellationToken).ConfigureAwait(false); + + return; // local functions static bool TokensMatch( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs index f4e3e0828a5d5..5037d55e9d1ad 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ConstructorSymbolReferenceFinder.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; -using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; @@ -32,80 +31,77 @@ protected override Task> DetermineGlobalAliasesAsync(IMet return GetAllMatchingGlobalAliasNamesAsync(project, containingType.Name, containingType.Arity, cancellationToken); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var containingType = symbol.ContainingType; var typeName = symbol.ContainingType.Name; - using var _ = ArrayBuilder.GetInstance(out var result); - await AddDocumentsAsync( - project, documents, typeName, result, cancellationToken).ConfigureAwait(false); + project, documents, typeName, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (globalAliases != null) { foreach (var globalAlias in globalAliases) { await AddDocumentsAsync( - project, documents, globalAlias, result, cancellationToken).ConfigureAwait(false); + project, documents, globalAlias, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } - result.AddRange(await FindDocumentsAsync( - project, documents, containingType.SpecialType.ToPredefinedType(), cancellationToken).ConfigureAwait(false)); - - result.AddRange(await FindDocumentsWithGlobalSuppressMessageAttributeAsync( - project, documents, cancellationToken).ConfigureAwait(false)); + await FindDocumentsAsync( + project, documents, containingType.SpecialType.ToPredefinedType(), processResult, processResultData, cancellationToken).ConfigureAwait(false); - result.AddRange(symbol.MethodKind == MethodKind.Constructor - ? await FindDocumentsWithImplicitObjectCreationExpressionAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return result.ToImmutableAndClear(); + if (symbol.MethodKind == MethodKind.Constructor) + { + await FindDocumentsWithImplicitObjectCreationExpressionAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); + } } - private static Task> FindDocumentsWithImplicitObjectCreationExpressionAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsImplicitObjectCreation, cancellationToken); + private static Task FindDocumentsWithImplicitObjectCreationExpressionAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsImplicitObjectCreation, processResult, processResultData, cancellationToken); - private static async Task AddDocumentsAsync( + private static async Task AddDocumentsAsync( Project project, IImmutableSet? documents, string typeName, - ArrayBuilder result, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, typeName).ConfigureAwait(false); - - var documentsWithAttribute = TryGetNameWithoutAttributeSuffix(typeName, project.Services.GetRequiredService(), out var simpleName) - ? await FindDocumentsAsync(project, documents, cancellationToken, simpleName).ConfigureAwait(false) - : []; + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, typeName).ConfigureAwait(false); - result.AddRange(documentsWithName); - result.AddRange(documentsWithAttribute); + if (TryGetNameWithoutAttributeSuffix(typeName, project.Services.GetRequiredService(), out var simpleName)) + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, simpleName).ConfigureAwait(false); } private static bool IsPotentialReference(PredefinedType predefinedType, ISyntaxFactsService syntaxFacts, SyntaxToken token) => syntaxFacts.TryGetPredefinedType(token, out var actualType) && predefinedType == actualType; - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _1 = ArrayBuilder.GetInstance(out var result); - // First just look for this normal constructor references using the name of it's containing type. var name = methodSymbol.ContainingType.Name; await AddReferencesInDocumentWorkerAsync( - methodSymbol, name, state, result, cancellationToken).ConfigureAwait(false); + methodSymbol, name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); // Next, look for constructor references through a global alias to our containing type. foreach (var globalAlias in state.GlobalAliases) @@ -117,67 +113,61 @@ await AddReferencesInDocumentWorkerAsync( continue; await AddReferencesInDocumentWorkerAsync( - methodSymbol, globalAlias, state, result, cancellationToken).ConfigureAwait(false); + methodSymbol, globalAlias, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - // Nest, our containing type might itself have local aliases to it in this particular file. - // If so, see what the local aliases are and then search for constructor references to that. - using var _2 = ArrayBuilder.GetInstance(out var typeReferences); - await NamedTypeSymbolReferenceFinder.AddReferencesToTypeOrGlobalAliasToItAsync( - methodSymbol.ContainingType, state, typeReferences, cancellationToken).ConfigureAwait(false); - - var aliasReferences = await FindLocalAliasReferencesAsync( - typeReferences, methodSymbol, state, cancellationToken).ConfigureAwait(false); - // Finally, look for constructor references to predefined types (like `new int()`), // implicit object references, and inside global suppression attributes. - result.AddRange(await FindPredefinedTypeReferencesAsync( - methodSymbol, state, cancellationToken).ConfigureAwait(false)); - - result.AddRange(await FindReferencesInImplicitObjectCreationExpressionAsync( - methodSymbol, state, cancellationToken).ConfigureAwait(false)); + await FindPredefinedTypeReferencesAsync( + methodSymbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - result.AddRange(await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - methodSymbol, state, cancellationToken).ConfigureAwait(false)); + await FindReferencesInImplicitObjectCreationExpressionAsync( + methodSymbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return result.ToImmutableAndClear(); + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + methodSymbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } /// /// Finds references to in this , but only if it referenced /// though (which might be the actual name of the type, or a global alias to it). /// - private static async Task AddReferencesInDocumentWorkerAsync( + private static async Task AddReferencesInDocumentWorkerAsync( IMethodSymbol symbol, string name, FindReferencesDocumentState state, - ArrayBuilder result, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - result.AddRange(await FindOrdinaryReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); - result.AddRange(await FindAttributeReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); + await FindOrdinaryReferencesAsync( + symbol, name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); + await FindAttributeReferencesAsync( + symbol, name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static ValueTask> FindOrdinaryReferencesAsync( + private static ValueTask FindOrdinaryReferencesAsync( IMethodSymbol symbol, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return FindReferencesInDocumentUsingIdentifierAsync( - symbol, name, state, cancellationToken); + symbol, name, state, processResult, processResultData, cancellationToken); } - private static ValueTask> FindPredefinedTypeReferencesAsync( + private static ValueTask FindPredefinedTypeReferencesAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var predefinedType = symbol.ContainingType.SpecialType.ToPredefinedType(); if (predefinedType == PredefinedType.None) - return new([]); + return ValueTaskFactory.CompletedTask; var tokens = state.Root .DescendantTokens(descendIntoTrivia: true) @@ -185,23 +175,27 @@ private static ValueTask> FindPredefinedTypeRefer static (token, tuple) => IsPotentialReference(tuple.predefinedType, tuple.state.SyntaxFacts, token), (state, predefinedType)); - return FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken); + return FindReferencesInTokensAsync(symbol, state, tokens, processResult, processResultData, cancellationToken); } - private static ValueTask> FindAttributeReferencesAsync( + private static ValueTask FindAttributeReferencesAsync( IMethodSymbol symbol, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return TryGetNameWithoutAttributeSuffix(name, state.SyntaxFacts, out var simpleName) - ? FindReferencesInDocumentUsingIdentifierAsync(symbol, simpleName, state, cancellationToken) - : new([]); + ? FindReferencesInDocumentUsingIdentifierAsync(symbol, simpleName, state, processResult, processResultData, cancellationToken) + : ValueTaskFactory.CompletedTask; } - private Task> FindReferencesInImplicitObjectCreationExpressionAsync( + private Task FindReferencesInImplicitObjectCreationExpressionAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { // Only check `new (...)` calls that supply enough arguments to match all the required parameters for the constructor. @@ -214,13 +208,13 @@ private Task> FindReferencesInImplicitObjectCreat ? -1 : symbol.Parameters.Length; - return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, cancellationToken); + return FindReferencesInDocumentAsync(state, IsRelevantDocument, CollectMatchingReferences, processResult, processResultData, cancellationToken); static bool IsRelevantDocument(SyntaxTreeIndex syntaxTreeInfo) => syntaxTreeInfo.ContainsImplicitObjectCreation; void CollectMatchingReferences( - SyntaxNode node, FindReferencesDocumentState state, ArrayBuilder locations) + SyntaxNode node, FindReferencesDocumentState state, Action processResult, TData processResultData) { var syntaxFacts = state.SyntaxFacts; if (!syntaxFacts.IsImplicitObjectCreationExpression(node)) @@ -241,9 +235,10 @@ void CollectMatchingReferences( var location = node.GetFirstToken().GetLocation(); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, new ReferenceLocation( + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: true, symbolUsageInfo, - GetAdditionalFindUsagesProperties(node, state), CandidateReason.None))); + GetAdditionalFindUsagesProperties(node, state), CandidateReason.None)); + processResult(result, processResultData); } } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.cs index 610db4da1aeac..a6c1623d5ba95 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/DestructorSymbolReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -15,23 +16,27 @@ internal sealed class DestructorSymbolReferenceFinder : AbstractReferenceFinder< protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind == MethodKind.Destructor; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; } - protected override ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( IMethodSymbol methodSymbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return new ValueTask>([]); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.cs index 756bffd4366c4..8784ccd1b37ef 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/EventSymbolReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Linq; @@ -33,25 +34,28 @@ protected sealed override ValueTask> DetermineCascadedSy return new(backingFields.Concat(associatedNamedTypes)); } - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IEventSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithName.Concat(documentsWithGlobalAttributes); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected sealed override ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IEventSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return FindReferencesInDocumentUsingSymbolNameAsync(symbol, state, cancellationToken); + return FindReferencesInDocumentUsingSymbolNameAsync(symbol, state, processResult, processResultData, cancellationToken); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs index 7059fefdf72ab..4078ddcbba83d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitConversionSymbolReferenceFinder.cs @@ -2,9 +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. +using System; using System.Collections.Generic; using System.Collections.Immutable; -using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; @@ -23,11 +23,13 @@ protected override bool CanFind(IMethodSymbol symbol) private static INamedTypeSymbol? GetUnderlyingNamedType(ITypeSymbol symbol) => UnderlyingNamedTypeVisitor.Instance.Visit(symbol); - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -43,25 +45,25 @@ protected sealed override async Task> DetermineDocument var underlyingNamedType = GetUnderlyingNamedType(symbol.ReturnType); Contract.ThrowIfNull(underlyingNamedType); - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, underlyingNamedType.Name).ConfigureAwait(false); - var documentsWithType = await FindDocumentsAsync(project, documents, underlyingNamedType.SpecialType.ToPredefinedType(), cancellationToken).ConfigureAwait(false); - using var _ = ArrayBuilder.GetInstance(out var result); + using var _ = PooledHashSet.GetInstance(out var result); + await FindDocumentsAsync(project, documents, StandardCallbacks.AddToHashSet, result, cancellationToken, underlyingNamedType.Name).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, underlyingNamedType.SpecialType.ToPredefinedType(), StandardCallbacks.AddToHashSet, result, cancellationToken).ConfigureAwait(false); // Ignore any documents that don't also have an explicit cast in them. - foreach (var document in documentsWithName.Concat(documentsWithType).Distinct()) + foreach (var document in result) { var index = await SyntaxTreeIndex.GetRequiredIndexAsync(document, cancellationToken).ConfigureAwait(false); if (index.ContainsConversion) - result.Add(document); + processResult(document, processResultData); } - - return result.ToImmutableAndClear(); } - protected sealed override ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -71,7 +73,7 @@ protected sealed override ValueTask> FindReferenc static (token, state) => IsPotentialReference(state.SyntaxFacts, token), state); - return FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken); + return FindReferencesInTokensAsync(symbol, state, tokens, processResult, processResultData, cancellationToken); } private static bool IsPotentialReference( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.cs index 51256d807e4b0..a92e4f0ca1eba 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ExplicitInterfaceMethodReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -15,25 +16,29 @@ internal sealed class ExplicitInterfaceMethodReferenceFinder : AbstractReference protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind == MethodKind.ExplicitInterfaceImplementation; - protected sealed override Task> DetermineDocumentsToSearchAsync( + protected sealed override Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { // An explicit method can't be referenced anywhere. - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; } - protected sealed override ValueTask> FindReferencesInDocumentAsync( + protected sealed override ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { // An explicit method can't be referenced anywhere. - return new([]); + return ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs index 43c3c03d7ccdc..11c23a50ec558 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/FieldSymbolReferenceFinder.cs @@ -2,11 +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. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -26,29 +26,31 @@ protected override ValueTask> DetermineCascadedSymbolsAs : new(ImmutableArray.Empty); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IFieldSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithName.Concat(documentsWithGlobalAttributes); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( IFieldSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var nameReferences = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - return nameReferences.Concat(suppressionReferences); + await FindReferencesInDocumentUsingSymbolNameAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.cs index 395fcb5402ca3..f5b717542dc20 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/IReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -49,9 +50,10 @@ ValueTask> DetermineCascadedSymbolsAsync( /// /// Implementations of this method must be thread-safe. /// - Task> DetermineDocumentsToSearchAsync( + Task DetermineDocumentsToSearchAsync( ISymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); /// @@ -61,9 +63,11 @@ Task> DetermineDocumentsToSearchAsync( /// /// Implementations of this method must be thread-safe. /// - ValueTask> FindReferencesInDocumentAsync( + ValueTask FindReferencesInDocumentAsync( ISymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.cs index 78d29392e7082..55150a8b42bf7 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/MethodTypeParameterSymbolReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -36,11 +37,13 @@ protected override ValueTask> DetermineCascadedSymbolsAs return new([]); } - protected sealed override Task> DetermineDocumentsToSearchAsync( + protected sealed override Task DetermineDocumentsToSearchAsync( ITypeParameterSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -55,7 +58,7 @@ protected sealed override Task> DetermineDocumentsToSea // Also, we only look for files that have the name of the owning type. This helps filter // down the set considerably. Contract.ThrowIfNull(symbol.DeclaringMethod); - return FindDocumentsAsync(project, documents, cancellationToken, symbol.Name, + return FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name, GetMemberNameWithoutInterfaceName(symbol.DeclaringMethod.Name), symbol.DeclaringMethod.ContainingType.Name); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.cs index 74537c547319e..35816840c7d6c 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamedTypeSymbolReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Linq; @@ -49,54 +50,49 @@ private static void Add(ArrayBuilder result, ImmutableArray()); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( INamedTypeSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var result); - - await AddDocumentsToSearchAsync(symbol.Name, project, documents, result, cancellationToken).ConfigureAwait(false); + await AddDocumentsToSearchAsync(symbol.Name, project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (globalAliases != null) { foreach (var alias in globalAliases) - await AddDocumentsToSearchAsync(alias, project, documents, result, cancellationToken).ConfigureAwait(false); + await AddDocumentsToSearchAsync(alias, project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - result.AddRange(await FindDocumentsAsync( - project, documents, symbol.SpecialType.ToPredefinedType(), cancellationToken).ConfigureAwait(false)); - - result.AddRange(await FindDocumentsWithGlobalSuppressMessageAttributeAsync( - project, documents, cancellationToken).ConfigureAwait(false)); + await FindDocumentsAsync( + project, documents, symbol.SpecialType.ToPredefinedType(), processResult, processResultData, cancellationToken).ConfigureAwait(false); - return result.ToImmutableAndClear(); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } /// /// Looks for documents likely containing in them. That name will either be the actual /// name of the named type we're looking for, or it might be a global alias to it. /// - private static async Task AddDocumentsToSearchAsync( + private static async Task AddDocumentsToSearchAsync( string throughName, Project project, IImmutableSet? documents, - ArrayBuilder result, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var syntaxFacts = project.Services.GetRequiredService(); - var documentsWithName = await FindDocumentsAsync( - project, documents, cancellationToken, throughName).ConfigureAwait(false); - - var documentsWithAttribute = TryGetNameWithoutAttributeSuffix(throughName, syntaxFacts, out var simpleName) - ? await FindDocumentsAsync(project, documents, cancellationToken, simpleName).ConfigureAwait(false) - : []; + await FindDocumentsAsync( + project, documents, processResult, processResultData, cancellationToken, throughName).ConfigureAwait(false); - result.AddRange(documentsWithName); - result.AddRange(documentsWithAttribute); + if (TryGetNameWithoutAttributeSuffix(throughName, syntaxFacts, out var simpleName)) + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, simpleName).ConfigureAwait(false); } private static bool IsPotentialReference( @@ -109,9 +105,11 @@ private static bool IsPotentialReference( predefinedType == actualType; } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( INamedTypeSymbol namedType, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -120,31 +118,34 @@ protected override async ValueTask> FindReference // First find all references to this type, either with it's actual name, or through potential // global alises to it. await AddReferencesToTypeOrGlobalAliasToItAsync( - namedType, state, initialReferences, cancellationToken).ConfigureAwait(false); + namedType, state, StandardCallbacks.AddToArrayBuilder, initialReferences, cancellationToken).ConfigureAwait(false); + + // The items in initialReferences need to be both reported and used later to calculate additional results. + foreach (var location in initialReferences) + processResult(location, processResultData); // This named type may end up being locally aliased as well. If so, now find all the references // to the local alias. - initialReferences.AddRange(await FindLocalAliasReferencesAsync( - initialReferences, state, cancellationToken).ConfigureAwait(false)); - - initialReferences.AddRange(await FindPredefinedTypeReferencesAsync( - namedType, state, cancellationToken).ConfigureAwait(false)); + await FindLocalAliasReferencesAsync( + initialReferences, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - initialReferences.AddRange(await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - namedType, state, cancellationToken).ConfigureAwait(false)); + await FindPredefinedTypeReferencesAsync( + namedType, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return initialReferences.ToImmutableAndClear(); + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + namedType, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - internal static async ValueTask AddReferencesToTypeOrGlobalAliasToItAsync( + internal static async ValueTask AddReferencesToTypeOrGlobalAliasToItAsync( INamedTypeSymbol namedType, FindReferencesDocumentState state, - ArrayBuilder nonAliasReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { await AddNonAliasReferencesAsync( - namedType, namedType.Name, state, nonAliasReferences, cancellationToken).ConfigureAwait(false); + namedType, namedType.Name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); foreach (var globalAlias in state.GlobalAliases) { @@ -155,7 +156,7 @@ await AddNonAliasReferencesAsync( continue; await AddNonAliasReferencesAsync( - namedType, globalAlias, state, nonAliasReferences, cancellationToken).ConfigureAwait(false); + namedType, globalAlias, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } @@ -164,24 +165,27 @@ await AddNonAliasReferencesAsync( /// only if it referenced though (which might be the actual name /// of the type, or a global alias to it). /// - private static async ValueTask AddNonAliasReferencesAsync( + private static async ValueTask AddNonAliasReferencesAsync( INamedTypeSymbol symbol, string name, FindReferencesDocumentState state, - ArrayBuilder nonAliasesReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { - nonAliasesReferences.AddRange(await FindOrdinaryReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); + await FindOrdinaryReferencesAsync( + symbol, name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - nonAliasesReferences.AddRange(await FindAttributeReferencesAsync( - symbol, name, state, cancellationToken).ConfigureAwait(false)); + await FindAttributeReferencesAsync( + symbol, name, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static ValueTask> FindOrdinaryReferencesAsync( + private static ValueTask FindOrdinaryReferencesAsync( INamedTypeSymbol namedType, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { // Get the parent node that best matches what this token represents. For example, if we have `new a.b()` @@ -190,17 +194,19 @@ private static ValueTask> FindOrdinaryReferencesA // associate with the type, but rather with the constructor itself. return FindReferencesInDocumentUsingIdentifierAsync( - namedType, name, state, cancellationToken); + namedType, name, state, processResult, processResultData, cancellationToken); } - private static ValueTask> FindPredefinedTypeReferencesAsync( + private static ValueTask FindPredefinedTypeReferencesAsync( INamedTypeSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var predefinedType = symbol.SpecialType.ToPredefinedType(); if (predefinedType == PredefinedType.None) - return new([]); + return ValueTaskFactory.CompletedTask; var tokens = state.Root .DescendantTokens(descendIntoTrivia: true) @@ -208,17 +214,19 @@ private static ValueTask> FindPredefinedTypeRefer static (token, tuple) => IsPotentialReference(tuple.predefinedType, tuple.state.SyntaxFacts, token), (state, predefinedType)); - return FindReferencesInTokensAsync(symbol, state, tokens, cancellationToken); + return FindReferencesInTokensAsync(symbol, state, tokens, processResult, processResultData, cancellationToken); } - private static ValueTask> FindAttributeReferencesAsync( + private static ValueTask FindAttributeReferencesAsync( INamedTypeSymbol namedType, string name, FindReferencesDocumentState state, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { return TryGetNameWithoutAttributeSuffix(name, state.SyntaxFacts, out var nameWithoutSuffix) - ? FindReferencesInDocumentUsingIdentifierAsync(namedType, nameWithoutSuffix, state, cancellationToken) - : new([]); + ? FindReferencesInDocumentUsingIdentifierAsync(namedType, nameWithoutSuffix, state, processResult, processResultData, cancellationToken) + : ValueTaskFactory.CompletedTask; } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.cs index dc0f2a36f79a3..e08a522c68576 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/NamespaceSymbolReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -22,53 +23,53 @@ protected override Task> DetermineGlobalAliasesAsync(INam return GetAllMatchingGlobalAliasNamesAsync(project, symbol.Name, arity: 0, cancellationToken); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( INamespaceSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var result); - - result.AddRange(!symbol.IsGlobalNamespace - ? await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false) - : await FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsGlobalKeyword, cancellationToken).ConfigureAwait(false)); + if (!symbol.IsGlobalNamespace) + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); + else + await FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsGlobalKeyword, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (globalAliases != null) { foreach (var globalAlias in globalAliases) { - result.AddRange(await FindDocumentsAsync( - project, documents, cancellationToken, globalAlias).ConfigureAwait(false)); + await FindDocumentsAsync( + project, documents, processResult, processResultData, cancellationToken, globalAlias).ConfigureAwait(false); } } - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - result.AddRange(documentsWithGlobalAttributes); - - return result.ToImmutableAndClear(); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( INamespaceSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - using var _ = ArrayBuilder.GetInstance(out var initialReferences); - if (symbol.IsGlobalNamespace) { await AddGlobalNamespaceReferencesAsync( - symbol, state, initialReferences, cancellationToken).ConfigureAwait(false); + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } else { + using var _ = ArrayBuilder.GetInstance(out var initialReferences); + var namespaceName = symbol.Name; await AddNamedReferencesAsync( - symbol, namespaceName, state, initialReferences, cancellationToken).ConfigureAwait(false); + symbol, namespaceName, state, StandardCallbacks.AddToArrayBuilder, initialReferences, cancellationToken).ConfigureAwait(false); foreach (var globalAlias in state.GlobalAliases) { @@ -79,41 +80,45 @@ await AddNamedReferencesAsync( continue; await AddNamedReferencesAsync( - symbol, globalAlias, state, initialReferences, cancellationToken).ConfigureAwait(false); + symbol, globalAlias, state, StandardCallbacks.AddToArrayBuilder, initialReferences, cancellationToken).ConfigureAwait(false); } - initialReferences.AddRange(await FindLocalAliasReferencesAsync( - initialReferences, symbol, state, cancellationToken).ConfigureAwait(false)); + // The items in initialReferences need to be both reported and used later to calculate additional results. + foreach (var location in initialReferences) + processResult(location, processResultData); - initialReferences.AddRange(await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false)); - } + await FindLocalAliasReferencesAsync( + initialReferences, symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return initialReferences.ToImmutableAndClear(); + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); + } } /// /// Finds references to in this , but only if it referenced /// though (which might be the actual name of the type, or a global alias to it). /// - private static async ValueTask AddNamedReferencesAsync( + private static async ValueTask AddNamedReferencesAsync( INamespaceSymbol symbol, string name, FindReferencesDocumentState state, - ArrayBuilder initialReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var tokens = await FindMatchingIdentifierTokensAsync( state, name, cancellationToken).ConfigureAwait(false); - initialReferences.AddRange(await FindReferencesInTokensAsync( - symbol, state, tokens, cancellationToken).ConfigureAwait(false)); + await FindReferencesInTokensAsync( + symbol, state, tokens, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static async Task AddGlobalNamespaceReferencesAsync( + private static async Task AddGlobalNamespaceReferencesAsync( INamespaceSymbol symbol, FindReferencesDocumentState state, - ArrayBuilder initialReferences, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { var tokens = state.Root @@ -122,7 +127,7 @@ private static async Task AddGlobalNamespaceReferencesAsync( static (token, state) => state.SyntaxFacts.IsGlobalNamespaceKeyword(token), state); - initialReferences.AddRange(await FindReferencesInTokensAsync( - symbol, state, tokens, cancellationToken).ConfigureAwait(false)); + await FindReferencesInTokensAsync( + symbol, state, tokens, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs index a4135e8d5f065..5b3b92945dc16 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OperatorSymbolReferenceFinder.cs @@ -2,13 +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. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -17,36 +17,41 @@ internal sealed class OperatorSymbolReferenceFinder : AbstractMethodOrPropertyOr protected override bool CanFind(IMethodSymbol symbol) => symbol.MethodKind is MethodKind.UserDefinedOperator or MethodKind.BuiltinOperator; - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { var op = symbol.GetPredefinedOperator(); - var documentsWithOp = await FindDocumentsAsync(project, documents, op, cancellationToken).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithOp.Concat(documentsWithGlobalAttributes); + await FindDocumentsAsync(project, documents, op, processResult, processResultData, cancellationToken).ConfigureAwait(false); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static Task> FindDocumentsAsync( + private static Task FindDocumentsAsync( Project project, IImmutableSet? documents, PredefinedOperator op, + Action processResult, + TData processResultData, CancellationToken cancellationToken) { if (op == PredefinedOperator.None) - return SpecializedTasks.EmptyImmutableArray(); + return Task.CompletedTask; return FindDocumentsWithPredicateAsync( - project, documents, static (index, op) => index.ContainsPredefinedOperator(op), op, cancellationToken); + project, documents, static (index, op) => index.ContainsPredefinedOperator(op), op, processResult, processResultData, cancellationToken); } - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -57,12 +62,10 @@ protected sealed override async ValueTask> FindRe static (token, tuple) => IsPotentialReference(tuple.state.SyntaxFacts, tuple.op, token), (state, op)); - var opReferences = await FindReferencesInTokensAsync( - symbol, state, tokens, cancellationToken).ConfigureAwait(false); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - - return opReferences.Concat(suppressionReferences); + await FindReferencesInTokensAsync( + symbol, state, tokens, processResult, processResultData, cancellationToken).ConfigureAwait(false); + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } private static bool IsPotentialReference( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs index 91ab4963aa113..1131b5489010d 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/OrdinaryMethodReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -42,11 +43,13 @@ private static ImmutableArray GetOtherPartsOfPartial(IMethodSymbol symb return []; } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IMethodSymbol methodSymbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -65,38 +68,32 @@ protected override async Task> DetermineDocumentsToSear // searches for these, then we should find usages of 'lock(goo)' or 'synclock(goo)' // since they implicitly call those methods. - var ordinaryDocuments = await FindDocumentsAsync(project, documents, cancellationToken, methodSymbol.Name).ConfigureAwait(false); - var forEachDocuments = IsForEachMethod(methodSymbol) - ? await FindDocumentsWithForEachStatementsAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, methodSymbol.Name).ConfigureAwait(false); - var deconstructDocuments = IsDeconstructMethod(methodSymbol) - ? await FindDocumentsWithDeconstructionAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachMethod(methodSymbol)) + await FindDocumentsWithForEachStatementsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var awaitExpressionDocuments = IsGetAwaiterMethod(methodSymbol) - ? await FindDocumentsWithAwaitExpressionAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (IsDeconstructMethod(methodSymbol)) + await FindDocumentsWithDeconstructionAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync( - project, documents, cancellationToken).ConfigureAwait(false); + if (IsGetAwaiterMethod(methodSymbol)) + await FindDocumentsWithAwaitExpressionAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var documentsWithCollectionInitializers = IsAddMethod(methodSymbol) - ? await FindDocumentsWithCollectionInitializersAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + await FindDocumentsWithGlobalSuppressMessageAttributeAsync( + project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - return ordinaryDocuments.Concat( - forEachDocuments, deconstructDocuments, awaitExpressionDocuments, documentsWithGlobalAttributes, documentsWithCollectionInitializers); + if (IsAddMethod(methodSymbol)) + await FindDocumentsWithCollectionInitializersAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static Task> FindDocumentsWithDeconstructionAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsDeconstruction, cancellationToken); + private static Task FindDocumentsWithDeconstructionAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsDeconstruction, processResult, processResultData, cancellationToken); - private static Task> FindDocumentsWithAwaitExpressionAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsAwait, cancellationToken); + private static Task FindDocumentsWithAwaitExpressionAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsAwait, processResult, processResultData, cancellationToken); - private static Task> FindDocumentsWithCollectionInitializersAsync(Project project, IImmutableSet? documents, CancellationToken cancellationToken) - => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsCollectionInitializer, cancellationToken); + private static Task FindDocumentsWithCollectionInitializersAsync(Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) + => FindDocumentsWithPredicateAsync(project, documents, static index => index.ContainsCollectionInitializer, processResult, processResultData, cancellationToken); private static bool IsForEachMethod(IMethodSymbol methodSymbol) => methodSymbol.Name is WellKnownMemberNames.GetEnumeratorMethodName or @@ -111,34 +108,30 @@ private static bool IsGetAwaiterMethod(IMethodSymbol methodSymbol) private static bool IsAddMethod(IMethodSymbol methodSymbol) => methodSymbol.Name == WellKnownMemberNames.CollectionInitializerAddMethodName; - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var nameMatches = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); + await FindReferencesInDocumentUsingSymbolNameAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var forEachMatches = IsForEachMethod(symbol) - ? await FindReferencesInForEachStatementsAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachMethod(symbol)) + await FindReferencesInForEachStatementsAsync(symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var deconstructMatches = IsDeconstructMethod(symbol) - ? await FindReferencesInDeconstructionAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsDeconstructMethod(symbol)) + await FindReferencesInDeconstructionAsync(symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var getAwaiterMatches = IsGetAwaiterMethod(symbol) - ? await FindReferencesInAwaitExpressionAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (IsGetAwaiterMethod(symbol)) + await FindReferencesInAwaitExpressionAsync(symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var addMatches = IsAddMethod(symbol) - ? await FindReferencesInCollectionInitializerAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; - - return nameMatches.Concat(forEachMatches, deconstructMatches, getAwaiterMatches, suppressionReferences, addMatches); + if (IsAddMethod(symbol)) + await FindReferencesInCollectionInitializerAsync(symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs index c15af3ce4355e..b13c380c6f9c9 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/ParameterSymbolReferenceFinder.cs @@ -20,11 +20,13 @@ internal sealed class ParameterSymbolReferenceFinder : AbstractReferenceFinder true; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( IParameterSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -33,16 +35,18 @@ protected override Task> DetermineDocumentsToSearchAsyn // elsewhere as "paramName:" or "paramName:=". We can narrow the search by // filtering down to matches of that form. For now we just return any document // that references something with this name. - return FindDocumentsAsync(project, documents, cancellationToken, symbol.Name); + return FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name); } - protected override ValueTask> FindReferencesInDocumentAsync( + protected override ValueTask FindReferencesInDocumentAsync( IParameterSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - return FindReferencesInDocumentUsingIdentifierAsync(symbol, symbol.Name, state, cancellationToken); + return FindReferencesInDocumentUsingIdentifierAsync(symbol, symbol.Name, state, processResult, processResultData, cancellationToken); } protected override async ValueTask> DetermineCascadedSymbolsAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs index 383c82ad44096..7c12a31b19e68 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertyAccessorSymbolReferenceFinder.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. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; -using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; -using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FindSymbols.Finders; @@ -30,64 +29,66 @@ protected override ValueTask> DetermineCascadedSymbolsAs : new(ImmutableArray.Create(symbol.AssociatedSymbol)); } - protected override async Task> DetermineDocumentsToSearchAsync( + protected override async Task DetermineDocumentsToSearchAsync( IMethodSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { // First, find any documents with the full name of the accessor (i.e. get_Goo). // This will find explicit calls to the method (which can happen when C# references // a VB parameterized property). - var documentsWithName = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); - var propertyDocuments = ImmutableArray.Empty; if (symbol.AssociatedSymbol is IPropertySymbol property && options.AssociatePropertyReferencesWithSpecificAccessor) { // we want to associate normal property references with the specific accessor being // referenced. So we also need to include documents with our property's name. Just // defer to the Property finder to find these docs and combine them with the result. - propertyDocuments = await ReferenceFinders.Property.DetermineDocumentsToSearchAsync( + await ReferenceFinders.Property.DetermineDocumentsToSearchAsync( property, globalAliases, project, documents, + processResult, processResultData, options with { AssociatePropertyReferencesWithSpecificAccessor = false }, cancellationToken).ConfigureAwait(false); } - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return documentsWithName.Concat(propertyDocuments, documentsWithGlobalAttributes); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - protected override async ValueTask> FindReferencesInDocumentAsync( + protected override async ValueTask FindReferencesInDocumentAsync( IMethodSymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var references = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); + await FindReferencesInDocumentUsingSymbolNameAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); if (symbol.AssociatedSymbol is not IPropertySymbol property || !options.AssociatePropertyReferencesWithSpecificAccessor) { - return references; + return; } - var propertyReferences = await ReferenceFinders.Property.FindReferencesInDocumentAsync( - property, state, - options with { AssociatePropertyReferencesWithSpecificAccessor = false }, - cancellationToken).ConfigureAwait(false); - - var accessorReferences = propertyReferences.WhereAsArray( - loc => + await ReferenceFinders.Property.FindReferencesInDocumentAsync( + property, + state, + static (loc, data) => { var accessors = GetReferencedAccessorSymbols( - state, property, loc.Node, cancellationToken); - return accessors.Contains(symbol); - }); - - return references.Concat(accessorReferences); + data.state, data.property, loc.Node, data.cancellationToken); + if (accessors.Contains(data.symbol)) + data.processResult(loc, data.processResultData); + }, + (property, symbol, state, processResult, processResultData, cancellationToken), + options with { AssociatePropertyReferencesWithSpecificAccessor = false }, + cancellationToken).ConfigureAwait(false); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs index 6d2acf8ebeb77..cb70c9670db68 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/PropertySymbolReferenceFinder.cs @@ -91,87 +91,92 @@ private static void CascadeToPrimaryConstructorParameters(IPropertySymbol proper } } - protected sealed override async Task> DetermineDocumentsToSearchAsync( + protected sealed override async Task DetermineDocumentsToSearchAsync( IPropertySymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var ordinaryDocuments = await FindDocumentsAsync(project, documents, cancellationToken, symbol.Name).ConfigureAwait(false); + await FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name).ConfigureAwait(false); - var forEachDocuments = IsForEachProperty(symbol) - ? await FindDocumentsWithForEachStatementsAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachProperty(symbol)) + await FindDocumentsWithForEachStatementsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var elementAccessDocument = symbol.IsIndexer - ? await FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (symbol.IsIndexer) + await FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var indexerMemberCrefDocument = symbol.IsIndexer - ? await FindDocumentWithIndexerMemberCrefAsync(project, documents, cancellationToken).ConfigureAwait(false) - : []; + if (symbol.IsIndexer) + await FindDocumentWithIndexerMemberCrefAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var documentsWithGlobalAttributes = await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, cancellationToken).ConfigureAwait(false); - return ordinaryDocuments.Concat(forEachDocuments, elementAccessDocument, indexerMemberCrefDocument, documentsWithGlobalAttributes); + await FindDocumentsWithGlobalSuppressMessageAttributeAsync(project, documents, processResult, processResultData, cancellationToken).ConfigureAwait(false); } private static bool IsForEachProperty(IPropertySymbol symbol) => symbol.Name == WellKnownMemberNames.CurrentPropertyName; - protected sealed override async ValueTask> FindReferencesInDocumentAsync( + protected sealed override async ValueTask FindReferencesInDocumentAsync( IPropertySymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { - var nameReferences = await FindReferencesInDocumentUsingSymbolNameAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - - if (options.AssociatePropertyReferencesWithSpecificAccessor) - { - // We want to associate property references to a specific accessor (if an accessor - // is being referenced). Check if this reference would match an accessor. If so, do - // not add it. It will be added by PropertyAccessorSymbolReferenceFinder. - nameReferences = nameReferences.WhereAsArray(loc => + await FindReferencesInDocumentUsingSymbolNameAsync( + symbol, + state, + static (loc, data) => { - var accessors = GetReferencedAccessorSymbols( - state, symbol, loc.Node, cancellationToken); - return accessors.IsEmpty; - }); - } + var useResult = true; + if (data.options.AssociatePropertyReferencesWithSpecificAccessor) + { + // We want to associate property references to a specific accessor (if an accessor + // is being referenced). Check if this reference would match an accessor. If so, do + // not add it. It will be added by PropertyAccessorSymbolReferenceFinder. + var accessors = GetReferencedAccessorSymbols( + data.state, data.symbol, loc.Node, data.cancellationToken); + useResult = accessors.IsEmpty; + } - var forEachReferences = IsForEachProperty(symbol) - ? await FindReferencesInForEachStatementsAsync(symbol, state, cancellationToken).ConfigureAwait(false) - : []; + if (useResult) + data.processResult(loc, data.processResultData); + }, + processResultData: (self: this, symbol, state, processResult, processResultData, options, cancellationToken), + cancellationToken).ConfigureAwait(false); - var indexerReferences = symbol.IsIndexer - ? await FindIndexerReferencesAsync(symbol, state, options, cancellationToken).ConfigureAwait(false) - : []; + if (IsForEachProperty(symbol)) + await FindReferencesInForEachStatementsAsync(symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); - var suppressionReferences = await FindReferencesInDocumentInsideGlobalSuppressionsAsync( - symbol, state, cancellationToken).ConfigureAwait(false); - return nameReferences.Concat(forEachReferences, indexerReferences, suppressionReferences); + if (symbol.IsIndexer) + await FindIndexerReferencesAsync(symbol, state, processResult, processResultData, options, cancellationToken).ConfigureAwait(false); + + await FindReferencesInDocumentInsideGlobalSuppressionsAsync( + symbol, state, processResult, processResultData, cancellationToken).ConfigureAwait(false); } - private static Task> FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync( - Project project, IImmutableSet? documents, CancellationToken cancellationToken) + private static Task FindDocumentWithExplicitOrImplicitElementAccessExpressionsAsync( + Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( - project, documents, static index => index.ContainsExplicitOrImplicitElementAccessExpression, cancellationToken); + project, documents, static index => index.ContainsExplicitOrImplicitElementAccessExpression, processResult, processResultData, cancellationToken); } - private static Task> FindDocumentWithIndexerMemberCrefAsync( - Project project, IImmutableSet? documents, CancellationToken cancellationToken) + private static Task FindDocumentWithIndexerMemberCrefAsync( + Project project, IImmutableSet? documents, Action processResult, TData processResultData, CancellationToken cancellationToken) { return FindDocumentsWithPredicateAsync( - project, documents, static index => index.ContainsIndexerMemberCref, cancellationToken); + project, documents, static index => index.ContainsIndexerMemberCref, processResult, processResultData, cancellationToken); } - private static async Task> FindIndexerReferencesAsync( + private static async Task FindIndexerReferencesAsync( IPropertySymbol symbol, FindReferencesDocumentState state, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -179,7 +184,7 @@ private static async Task> FindIndexerReferencesA { // Looking for individual get/set references. Don't find anything here. // these results will be provided by the PropertyAccessorSymbolReferenceFinder - return []; + return; } var syntaxFacts = state.SyntaxFacts; @@ -190,7 +195,6 @@ private static async Task> FindIndexerReferencesA syntaxFacts.IsImplicitElementAccess(node) || syntaxFacts.IsConditionalAccessExpression(node) || syntaxFacts.IsIndexerMemberCref(node)); - using var _ = ArrayBuilder.GetInstance(out var locations); foreach (var node in indexerReferenceExpressions) { @@ -204,14 +208,13 @@ private static async Task> FindIndexerReferencesA var location = state.SyntaxTree.GetLocation(new TextSpan(indexerReference.SpanStart, 0)); var symbolUsageInfo = GetSymbolUsageInfo(node, state, cancellationToken); - locations.Add(new FinderLocation(node, + var result = new FinderLocation(node, new ReferenceLocation( state.Document, alias: null, location, isImplicit: false, symbolUsageInfo, GetAdditionalFindUsagesProperties(node, state), - candidateReason))); + candidateReason)); + processResult(result, processResultData); } - - return locations.ToImmutableAndClear(); } private static ValueTask<(bool matched, CandidateReason reason, SyntaxNode indexerReference)> ComputeIndexerInformationAsync( diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.cs index 32587f95c601e..6b1ffc51734be 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/Finders/TypeParameterSymbolReferenceFinder.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.Generic; using System.Collections.Immutable; using System.Threading; @@ -14,11 +15,13 @@ internal sealed class TypeParameterSymbolReferenceFinder : AbstractTypeParameter protected override bool CanFind(ITypeParameterSymbol symbol) => symbol.TypeParameterKind != TypeParameterKind.Method; - protected override Task> DetermineDocumentsToSearchAsync( + protected override Task DetermineDocumentsToSearchAsync( ITypeParameterSymbol symbol, HashSet? globalAliases, Project project, IImmutableSet? documents, + Action processResult, + TData processResultData, FindReferencesSearchOptions options, CancellationToken cancellationToken) { @@ -29,6 +32,6 @@ protected override Task> DetermineDocumentsToSearchAsyn // parameter has a different name in different parts that we won't find it. However, // this only happens in error situations. It is not legal in C# to use a different // name for a type parameter in different parts. - return FindDocumentsAsync(project, documents, cancellationToken, symbol.Name, symbol.ContainingType.Name); + return FindDocumentsAsync(project, documents, processResult, processResultData, cancellationToken, symbol.Name, symbol.ContainingType.Name); } } diff --git a/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StandardCallbacks.cs b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StandardCallbacks.cs new file mode 100644 index 0000000000000..5696c32d2e8b9 --- /dev/null +++ b/src/Workspaces/Core/Portable/FindSymbols/FindReferences/StandardCallbacks.cs @@ -0,0 +1,18 @@ +// 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 Microsoft.CodeAnalysis.PooledObjects; + +namespace Microsoft.CodeAnalysis.FindSymbols; + +internal static class StandardCallbacks +{ + public static readonly Action> AddToHashSet = + static (data, set) => set.Add(data); + + public static readonly Action> AddToArrayBuilder = + static (data, builder) => builder.Add(data); +} diff --git a/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs b/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs index 9c62cd2c19139..9018ef3297747 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/StreamingProgressCollector.cs @@ -42,7 +42,7 @@ public ImmutableArray GetReferencedSymbols() { var result = new FixedSizeArrayBuilder(_symbolToLocations.Count); foreach (var (symbol, locations) in _symbolToLocations) - result.Add(new ReferencedSymbol(symbol, locations.ToImmutableArray())); + result.Add(new ReferencedSymbol(symbol, [.. locations])); return result.MoveToImmutable(); } diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs index 89be7cdadb127..50b7dd3af8c58 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.FindReferencesServerCallback.cs @@ -67,7 +67,7 @@ public async ValueTask OnDefinitionFoundAsync(SerializableSymbolGroup dehydrated map[symbolAndProjectId] = symbol; } - var symbolGroup = new SymbolGroup(map.Values.ToImmutableArray()); + var symbolGroup = new SymbolGroup([.. map.Values]); lock (_gate) { _groupMap[dehydrated] = symbolGroup; diff --git a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs index f69e50e9a3529..89ac7f1794849 100644 --- a/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs +++ b/src/Workspaces/Core/Portable/FindSymbols/SymbolFinder.cs @@ -308,6 +308,6 @@ internal static async Task> FindLinkedSymbolsAsync( } } - return linkedSymbols.ToImmutableArray(); + return [.. linkedSymbols]; } } diff --git a/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs b/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs index 6603464b13a8e..1f7fb23a853f3 100644 --- a/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs +++ b/src/Workspaces/Core/Portable/LinkedFileDiffMerging/LinkedFileDiffMergingSession.cs @@ -83,7 +83,7 @@ private async Task MergeLinkedDocumentGroupAsync( appliedChanges = await AddDocumentMergeChangesAsync( oldSolution.GetDocument(documentId), newSolution.GetDocument(documentId), - appliedChanges.ToList(), + [.. appliedChanges], unmergedChanges, groupSessionInfo, textDifferencingService, diff --git a/src/Workspaces/Core/Portable/Log/AggregateLogger.cs b/src/Workspaces/Core/Portable/Log/AggregateLogger.cs index 19a74f1a4972d..6f20609433576 100644 --- a/src/Workspaces/Core/Portable/Log/AggregateLogger.cs +++ b/src/Workspaces/Core/Portable/Log/AggregateLogger.cs @@ -36,7 +36,7 @@ public static AggregateLogger Create(params ILogger[] loggers) set.Add(logger); } - return new AggregateLogger(set.ToImmutableArray()); + return new AggregateLogger([.. set]); } public static ILogger AddOrReplace(ILogger newLogger, ILogger oldLogger, Func predicate) @@ -81,7 +81,7 @@ public static ILogger AddOrReplace(ILogger newLogger, ILogger oldLogger, Func predicate) @@ -105,7 +105,7 @@ public static ILogger Remove(ILogger logger, Func predicate) return set.Single(); } - return new AggregateLogger(set.ToImmutableArray()); + return new AggregateLogger([.. set]); } private AggregateLogger(ImmutableArray loggers) diff --git a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj index 3c1b1a4561ae0..2ef8b99744fab 100644 --- a/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj +++ b/src/Workspaces/Core/Portable/Microsoft.CodeAnalysis.Workspaces.csproj @@ -105,7 +105,7 @@ - + @@ -131,7 +131,7 @@ - + diff --git a/src/Workspaces/Core/Portable/Remote/ISerializerService.cs b/src/Workspaces/Core/Portable/Remote/ISerializerService.cs index f6f74f5bae648..f56f5f0a89417 100644 --- a/src/Workspaces/Core/Portable/Remote/ISerializerService.cs +++ b/src/Workspaces/Core/Portable/Remote/ISerializerService.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.Serialization; internal interface ISerializerService : IWorkspaceService { - void Serialize(object value, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken); + void Serialize(object value, ObjectWriter writer, CancellationToken cancellationToken); void SerializeParseOptions(ParseOptions options, ObjectWriter writer); diff --git a/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs b/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs index 34190d5261e2c..0a0b5e3c63411 100644 --- a/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs +++ b/src/Workspaces/Core/Portable/Rename/ConflictEngine/ConflictResolver.Session.cs @@ -177,10 +177,13 @@ public async Task ResolveConflictsAsync() if (phase == 1) { - conflictLocations = conflictLocations.Concat(conflictResolution.RelatedLocations - .Where(loc => documentIdsThatGetsAnnotatedAndRenamed.Contains(loc.DocumentId) && loc.Type == RelatedLocationType.PossiblyResolvableConflict) - .Select(loc => new ConflictLocationInfo(loc))) - .ToImmutableHashSet(); + conflictLocations = + [ + .. conflictLocations, + .. conflictResolution.RelatedLocations + .Where(loc => documentIdsThatGetsAnnotatedAndRenamed.Contains(loc.DocumentId) && loc.Type == RelatedLocationType.PossiblyResolvableConflict) + .Select(loc => new ConflictLocationInfo(loc)), + ]; } // Set the documents with conflicts that need to be processed in the next phase. diff --git a/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs b/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs index 6c6546e908b97..7a71e5dd2b9a3 100644 --- a/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs +++ b/src/Workspaces/Core/Portable/Rename/ConflictEngine/RenamedSpansTracker.cs @@ -226,7 +226,7 @@ internal async Task SimplifyAsync(Solution solution, IEnumerable>(); foreach (var (docId, spans) in _documentToModifiedSpansMap) - builder.Add(docId, spans.ToImmutableArray()); + builder.Add(docId, [.. spans]); return builder.ToImmutable(); } @@ -238,7 +238,7 @@ public ImmutableDictionary> GetDocu foreach (var (docId, spans) in _documentToComplexifiedSpansMap) { builder.Add(docId, spans.SelectAsArray( - s => new ComplexifiedSpan(s.OriginalSpan, s.NewSpan, s.ModifiedSubSpans.ToImmutableArray()))); + s => new ComplexifiedSpan(s.OriginalSpan, s.NewSpan, [.. s.ModifiedSubSpans]))); } return builder.ToImmutable(); diff --git a/src/Workspaces/Core/Portable/Rename/Renamer.cs b/src/Workspaces/Core/Portable/Rename/Renamer.cs index 741bab2bd9cfa..cc3c90299437b 100644 --- a/src/Workspaces/Core/Portable/Rename/Renamer.cs +++ b/src/Workspaces/Core/Portable/Rename/Renamer.cs @@ -119,7 +119,7 @@ internal static async Task RenameDocumentAsync( if (document.Services.GetService() != null) { // Don't advertise that we can file rename generated documents that map to a different file. - return new RenameDocumentActionSet([], document.Id, document.Name, document.Folders.ToImmutableArray(), options); + return new RenameDocumentActionSet([], document.Id, document.Name, [.. document.Folders], options); } using var _ = ArrayBuilder.GetInstance(out var actions); @@ -143,7 +143,7 @@ internal static async Task RenameDocumentAsync( actions.ToImmutable(), document.Id, newDocumentName, - newDocumentFolders.ToImmutableArray(), + [.. newDocumentFolders], options); } diff --git a/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs b/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs index e17bcb1a26d4f..a7c02bba2bfc5 100644 --- a/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs +++ b/src/Workspaces/Core/Portable/Serialization/ISupportTemporaryStorage.cs @@ -8,10 +8,11 @@ namespace Microsoft.CodeAnalysis.Serialization; /// -/// This lets consumer to get to inner temporary storage that references use -/// as its shadow copy storage +/// Interface for services that support dumping their contents to memory-mapped-files (generally speaking, our assembly +/// reference objects). This allows those objects to expose the memory-mapped-file info needed to read that data back +/// in in any process. /// internal interface ISupportTemporaryStorage { - IReadOnlyList? GetStorages(); + IReadOnlyList? StorageHandles { get; } } diff --git a/src/Workspaces/Core/Portable/Serialization/PooledList.cs b/src/Workspaces/Core/Portable/Serialization/PooledList.cs deleted file mode 100644 index 3eb2680bb6702..0000000000000 --- a/src/Workspaces/Core/Portable/Serialization/PooledList.cs +++ /dev/null @@ -1,19 +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; -using System.Collections.Generic; - -namespace Microsoft.CodeAnalysis.Serialization; - -/// -/// This is just internal utility type to reduce allocations and redundant code -/// -internal static class Creator -{ - public static PooledObject> CreateList() - => SharedPools.Default>().GetPooledObject(); -} diff --git a/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs b/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs index 00c816d4c0e31..06e2b900e3035 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializableSourceText.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Immutable; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; -using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -16,6 +14,10 @@ using Roslyn.Utilities; using static Microsoft.CodeAnalysis.Host.TemporaryStorageService; +#if DEBUG +using System.Linq; +#endif + namespace Microsoft.CodeAnalysis.Serialization; #pragma warning disable CA1416 // Validate platform compatibility @@ -31,50 +33,49 @@ internal sealed class SerializableSourceText /// The storage location for . /// /// - /// Exactly one of or will be non-. + /// Exactly one of or will be non-. /// - private readonly TemporaryTextStorage? _storage; + private readonly TemporaryStorageTextHandle? _storageHandle; /// /// The in the current process. /// /// - /// + /// /// private readonly SourceText? _text; /// - /// The hash that would be produced by calling on . Can be passed in when already known to avoid unnecessary computation costs. + /// Weak reference to a SourceText computed from . Useful so that if multiple requests + /// come in for the source text, the same one can be returned as long as something is holding it alive. /// - public readonly ImmutableArray ContentHash; + private readonly WeakReference _computedText = new(target: null); /// - /// Weak reference to a SourceText computed from . Useful so that if multiple requests - /// come in for the source text, the same one can be returned as long as something is holding it alive. + /// Checksum of the contents (see ) of the text. /// - private readonly WeakReference _computedText = new(target: null); + public readonly Checksum ContentChecksum; - public SerializableSourceText(TemporaryTextStorage storage, ImmutableArray contentHash) - : this(storage, text: null, contentHash) + public SerializableSourceText(TemporaryStorageTextHandle storageHandle) + : this(storageHandle, text: null, storageHandle.ContentHash) { } public SerializableSourceText(SourceText text, ImmutableArray contentHash) - : this(storage: null, text, contentHash) + : this(storageHandle: null, text, contentHash) { } - private SerializableSourceText(TemporaryTextStorage? storage, SourceText? text, ImmutableArray contentHash) + private SerializableSourceText(TemporaryStorageTextHandle? storageHandle, SourceText? text, ImmutableArray contentHash) { - Debug.Assert(storage is null != text is null); + Debug.Assert(storageHandle is null != text is null); - _storage = storage; + _storageHandle = storageHandle; _text = text; - ContentHash = contentHash; + ContentChecksum = Checksum.Create(contentHash); #if DEBUG - var computedContentHash = TryGetText()?.GetContentHash() ?? _storage!.ContentHash; + var computedContentHash = TryGetText()?.GetContentHash() ?? _storageHandle!.ContentHash; Debug.Assert(contentHash.SequenceEqual(computedContentHash)); #endif } @@ -94,7 +95,7 @@ public async ValueTask GetTextAsync(CancellationToken cancellationTo return text; // Read and cache the text from the storage object so that other requests may see it if still kept alive by something. - text = await _storage!.ReadTextAsync(cancellationToken).ConfigureAwait(false); + text = await _storageHandle!.ReadFromTemporaryStorageAsync(cancellationToken).ConfigureAwait(false); _computedText.SetTarget(text); return text; } @@ -106,7 +107,7 @@ public SourceText GetText(CancellationToken cancellationToken) return text; // Read and cache the text from the storage object so that other requests may see it if still kept alive by something. - text = _storage!.ReadText(cancellationToken); + text = _storageHandle!.ReadFromTemporaryStorage(cancellationToken); _computedText.SetTarget(text); return text; } @@ -114,12 +115,22 @@ public SourceText GetText(CancellationToken cancellationToken) public static ValueTask FromTextDocumentStateAsync( TextDocumentState state, CancellationToken cancellationToken) { - if (state.Storage is TemporaryTextStorage storage) + if (state.TextAndVersionSource.TextLoader is SerializableSourceTextLoader serializableLoader) { - return new ValueTask(new SerializableSourceText(storage, storage.ContentHash)); + // If we're already pointing at a serializable loader, we can just use that directly. + return new(serializableLoader.SerializableSourceText); + } + else if (state.StorageHandle is TemporaryStorageTextHandle storageHandle) + { + // Otherwise, if we're pointing at a memory mapped storage location, we can create the source text that directly wraps that. + return new(new SerializableSourceText(storageHandle)); } else { + // Otherwise, the state object has reified the text into some other form, and dumped any original + // information on how it got it. In that case, we create a new text instance to represent the serializable + // source text out of. + return SpecializedTasks.TransformWithoutIntermediateCancellationExceptionAsync( static (state, cancellationToken) => state.GetTextAsync(cancellationToken), static (text, _) => new SerializableSourceText(text, text.GetContentHash()), @@ -128,66 +139,104 @@ public static ValueTask FromTextDocumentStateAsync( } } - public void Serialize(ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public void Serialize(ObjectWriter writer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - if (_storage is not null) - { - context.AddResource(_storage); - - writer.WriteInt32((int)_storage.ChecksumAlgorithm); - writer.WriteEncoding(_storage.Encoding); - writer.WriteByteArray(ImmutableCollectionsMarshal.AsArray(_storage.ContentHash)!); + if (_storageHandle is not null) + { writer.WriteInt32((int)SerializationKinds.MemoryMapFile); - writer.WriteString(_storage.Name); - writer.WriteInt64(_storage.Offset); - writer.WriteInt64(_storage.Size); + _storageHandle.Identifier.WriteTo(writer); + writer.WriteInt32((int)_storageHandle.ChecksumAlgorithm); + writer.WriteEncoding(_storageHandle.Encoding); + writer.WriteByteArray(ImmutableCollectionsMarshal.AsArray(_storageHandle.ContentHash)!); } else { RoslynDebug.AssertNotNull(_text); + writer.WriteInt32((int)SerializationKinds.Bits); writer.WriteInt32((int)_text.ChecksumAlgorithm); writer.WriteEncoding(_text.Encoding); writer.WriteByteArray(ImmutableCollectionsMarshal.AsArray(_text.GetContentHash())!); - writer.WriteInt32((int)SerializationKinds.Bits); _text.WriteTo(writer, cancellationToken); } } public static SerializableSourceText Deserialize( ObjectReader reader, - ITemporaryStorageServiceInternal storageService, + TemporaryStorageService storageService, ITextFactoryService textService, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var checksumAlgorithm = (SourceHashAlgorithm)reader.ReadInt32(); - var encoding = reader.ReadEncoding(); - var contentHash = ImmutableCollectionsMarshal.AsImmutableArray(reader.ReadByteArray()); - var kind = (SerializationKinds)reader.ReadInt32(); Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile); if (kind == SerializationKinds.MemoryMapFile) { - var storage2 = (TemporaryStorageService)storageService; - - var name = reader.ReadRequiredString(); - var offset = reader.ReadInt64(); - var size = reader.ReadInt64(); + var identifier = TemporaryStorageIdentifier.ReadFrom(reader); + var checksumAlgorithm = (SourceHashAlgorithm)reader.ReadInt32(); + var encoding = reader.ReadEncoding(); + var contentHash = ImmutableCollectionsMarshal.AsImmutableArray(reader.ReadByteArray()); + var storageHandle = storageService.GetTextHandle(identifier, checksumAlgorithm, encoding, contentHash); - var storage = storage2.AttachTemporaryTextStorage(name, offset, size, checksumAlgorithm, encoding, contentHash); - return new SerializableSourceText(storage, contentHash); + return new SerializableSourceText(storageHandle); } else { + var checksumAlgorithm = (SourceHashAlgorithm)reader.ReadInt32(); + var encoding = reader.ReadEncoding(); + var contentHash = ImmutableCollectionsMarshal.AsImmutableArray(reader.ReadByteArray()); + return new SerializableSourceText( SourceTextExtensions.ReadFrom(textService, reader, encoding, checksumAlgorithm, cancellationToken), contentHash); } } + + public TextLoader ToTextLoader(string? filePath) + => new SerializableSourceTextLoader(this, filePath); + + /// + /// A that wraps a and provides access to the text in + /// a deferred fashion. In practice, during a host and OOP sync, while all the documents will be 'serialized' over + /// to OOP, the actual contents of the documents will only need to be loaded depending on which files are open, and + /// thus what compilations and trees are needed. As such, we want to be able to lazily defer actually getting the + /// contents of the text until it's actually needed. This loader allows us to do that, allowing the OOP side to + /// simply point to the segments in the memory-mapped-file the host has dumped its text into, and only actually + /// realizing the real text values when they're needed. + /// + private sealed class SerializableSourceTextLoader : TextLoader + { + public readonly SerializableSourceText SerializableSourceText; + private readonly VersionStamp _version = VersionStamp.Create(); + + public SerializableSourceTextLoader( + SerializableSourceText serializableSourceText, + string? filePath) + { + SerializableSourceText = serializableSourceText; + FilePath = filePath; + } + + internal override string? FilePath { get; } + + /// + /// Documents should always hold onto instances of this text loader strongly. In other words, they should load + /// from this, and then dump the contents into a RecoverableText object that then dumps the contents to a memory + /// mapped file within this process. Doing that is pointless as the contents of this text are already in a + /// memory mapped file on the host side. + /// + internal override bool AlwaysHoldStrongly + => true; + + public override async Task LoadTextAndVersionAsync(LoadTextOptions options, CancellationToken cancellationToken) + => TextAndVersion.Create(await this.SerializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false), _version); + + internal override TextAndVersion LoadTextAndVersionSynchronously(LoadTextOptions options, CancellationToken cancellationToken) + => TextAndVersion.Create(this.SerializableSourceText.GetText(cancellationToken), _version); + } } diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService.cs index 9d94bccc485e8..e786280b8b51c 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService.cs @@ -6,16 +6,19 @@ using System.Collections.Concurrent; using System.Composition; using System.Linq; +using System.Runtime.Versioning; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Text; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Serialization; +#if NETCOREAPP +[SupportedOSPlatform("windows")] +#endif internal partial class SerializerService : ISerializerService { [ExportWorkspaceServiceFactory(typeof(ISerializerService), layer: ServiceLayer.Default), Shared] @@ -36,7 +39,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) private readonly SolutionServices _workspaceServices; - private readonly ITemporaryStorageServiceInternal _storageService; + private readonly TemporaryStorageService _storageService; private readonly ITextFactoryService _textService; private readonly IDocumentationProviderService? _documentationService; private readonly IAnalyzerAssemblyLoaderProvider _analyzerLoaderProvider; @@ -48,7 +51,9 @@ private protected SerializerService(SolutionServices workspaceServices) { _workspaceServices = workspaceServices; - _storageService = workspaceServices.GetRequiredService(); + // Serialization is only involved when we have a remote process. Which is only in VS. So the type of the + // storage service here is well known. + _storageService = (TemporaryStorageService)workspaceServices.GetRequiredService(); _textService = workspaceServices.GetRequiredService(); _analyzerLoaderProvider = workspaceServices.GetRequiredService(); _documentationService = workspaceServices.GetService(); @@ -75,7 +80,7 @@ public Checksum CreateChecksum(object value, CancellationToken cancellationToken case WellKnownSynchronizationKind.ParseOptions: case WellKnownSynchronizationKind.ProjectReference: case WellKnownSynchronizationKind.SourceGeneratedDocumentIdentity: - return Checksum.Create(value, this); + return Checksum.Create(value, this, cancellationToken); case WellKnownSynchronizationKind.MetadataReference: return CreateChecksum((MetadataReference)value, cancellationToken); @@ -84,7 +89,7 @@ public Checksum CreateChecksum(object value, CancellationToken cancellationToken return CreateChecksum((AnalyzerReference)value, cancellationToken); case WellKnownSynchronizationKind.SerializableSourceText: - return Checksum.Create(((SerializableSourceText)value).ContentHash); + throw new InvalidOperationException("Clients can already get a checksum directly from a SerializableSourceText"); default: // object that is not part of solution is not supported since we don't know what inputs are required to @@ -94,7 +99,7 @@ public Checksum CreateChecksum(object value, CancellationToken cancellationToken } } - public void Serialize(object value, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public void Serialize(object value, ObjectWriter writer, CancellationToken cancellationToken) { var kind = value.GetWellKnownSynchronizationKind(); @@ -134,7 +139,7 @@ public void Serialize(object value, ObjectWriter writer, SolutionReplicationCont return; case WellKnownSynchronizationKind.MetadataReference: - SerializeMetadataReference((MetadataReference)value, writer, context, cancellationToken); + SerializeMetadataReference((MetadataReference)value, writer, cancellationToken); return; case WellKnownSynchronizationKind.AnalyzerReference: @@ -142,7 +147,7 @@ public void Serialize(object value, ObjectWriter writer, SolutionReplicationCont return; case WellKnownSynchronizationKind.SerializableSourceText: - SerializeSourceText((SerializableSourceText)value, writer, context, cancellationToken); + SerializeSourceText((SerializableSourceText)value, writer, cancellationToken); return; case WellKnownSynchronizationKind.SolutionCompilationState: diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs index c3842c9ff3870..d7bbeba322659 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService_Asset.cs @@ -17,9 +17,9 @@ namespace Microsoft.CodeAnalysis.Serialization; /// internal partial class SerializerService { - private static void SerializeSourceText(SerializableSourceText text, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + private static void SerializeSourceText(SerializableSourceText text, ObjectWriter writer, CancellationToken cancellationToken) { - text.Serialize(writer, context, cancellationToken); + text.Serialize(writer, cancellationToken); } private void SerializeCompilationOptions(CompilationOptions options, ObjectWriter writer, CancellationToken cancellationToken) @@ -86,10 +86,10 @@ private static ProjectReference DeserializeProjectReference(ObjectReader reader, return new ProjectReference(projectId, aliases.ToImmutableArrayOrEmpty(), embedInteropTypes); } - private void SerializeMetadataReference(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + private void SerializeMetadataReference(MetadataReference reference, ObjectWriter writer, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - WriteMetadataReferenceTo(reference, writer, context, cancellationToken); + WriteMetadataReferenceTo(reference, writer, cancellationToken); } private MetadataReference DeserializeMetadataReference(ObjectReader reader, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs b/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs index f39c0d444450b..321876a3d0626 100644 --- a/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs +++ b/src/Workspaces/Core/Portable/Serialization/SerializerService_Reference.cs @@ -7,10 +7,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.IO; -using System.Linq; using System.Reflection.Metadata; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; @@ -19,12 +16,12 @@ namespace Microsoft.CodeAnalysis.Serialization; +using static TemporaryStorageService; + internal partial class SerializerService { private const int MetadataFailed = int.MaxValue; - private static readonly ConditionalWeakTable s_lifetimeMap = new(); - public static Checksum CreateChecksum(MetadataReference reference, CancellationToken cancellationToken) { if (reference is PortableExecutableReference portable) @@ -62,16 +59,15 @@ public static Checksum CreateChecksum(AnalyzerReference reference, CancellationT return Checksum.Create(stream); } - public virtual void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public virtual void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, CancellationToken cancellationToken) { if (reference is PortableExecutableReference portable) { - if (portable is ISupportTemporaryStorage supportTemporaryStorage) + if (portable is ISupportTemporaryStorage { StorageHandles: { Count: > 0 } handles } && + TryWritePortableExecutableReferenceBackedByTemporaryStorageTo( + portable, handles, writer, cancellationToken)) { - if (TryWritePortableExecutableReferenceBackedByTemporaryStorageTo(supportTemporaryStorage, writer, context, cancellationToken)) - { - return; - } + return; } WritePortableExecutableReferenceTo(portable, writer, cancellationToken); @@ -235,8 +231,7 @@ private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectRe var filePath = reader.ReadString(); - var tuple = TryReadMetadataFrom(reader, kind, cancellationToken); - if (tuple == null) + if (TryReadMetadataFrom(reader, kind, cancellationToken) is not (var metadata, var storageHandles)) { // TODO: deal with xml document provider properly // should we shadow copy xml doc comment? @@ -256,7 +251,7 @@ private PortableExecutableReference ReadPortableExecutableReferenceFrom(ObjectRe _documentationService.GetDocumentationProvider(filePath) : XmlDocumentationProvider.Default; return new SerializedMetadataReference( - properties, filePath, tuple.Value.metadata, tuple.Value.storages, documentProvider); + properties, filePath, metadata, storageHandles, documentProvider); } private static void WriteTo(MetadataReferenceProperties properties, ObjectWriter writer, CancellationToken cancellationToken) @@ -313,46 +308,28 @@ private static void WriteTo(Metadata? metadata, ObjectWriter writer, Cancellatio } private static bool TryWritePortableExecutableReferenceBackedByTemporaryStorageTo( - ISupportTemporaryStorage reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + PortableExecutableReference reference, + IReadOnlyList handles, + ObjectWriter writer, + CancellationToken cancellationToken) { - var storages = reference.GetStorages(); - if (storages == null) - { - return false; - } - - // Not clear if name should be allowed to be null here (https://github.com/dotnet/roslyn/issues/43037) - using var pooled = Creator.CreateList<(string? name, long offset, long size)>(); - - foreach (var storage in storages) - { - if (storage is not ITemporaryStorageWithName storage2) - { - return false; - } + Contract.ThrowIfTrue(handles.Count == 0); - context.AddResource(storage); - - pooled.Object.Add((storage2.Name, storage2.Offset, storage2.Size)); - } - - WritePortableExecutableReferenceHeaderTo((PortableExecutableReference)reference, SerializationKinds.MemoryMapFile, writer, cancellationToken); + WritePortableExecutableReferenceHeaderTo(reference, SerializationKinds.MemoryMapFile, writer, cancellationToken); writer.WriteInt32((int)MetadataImageKind.Assembly); - writer.WriteInt32(pooled.Object.Count); + writer.WriteInt32(handles.Count); - foreach (var (name, offset, size) in pooled.Object) + foreach (var handle in handles) { writer.WriteInt32((int)MetadataImageKind.Module); - writer.WriteString(name); - writer.WriteInt64(offset); - writer.WriteInt64(size); + handle.Identifier.WriteTo(writer); } return true; } - private (Metadata metadata, ImmutableArray storages)? TryReadMetadataFrom( + private (Metadata metadata, ImmutableArray storageHandles)? TryReadMetadataFrom( ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) { var imageKind = reader.ReadInt32(); @@ -363,160 +340,87 @@ private static bool TryWritePortableExecutableReferenceBackedByTemporaryStorageT } var metadataKind = (MetadataImageKind)imageKind; - if (_storageService == null) - { - if (metadataKind == MetadataImageKind.Assembly) - { - using var pooledMetadata = Creator.CreateList(); - - var count = reader.ReadInt32(); - for (var i = 0; i < count; i++) - { - metadataKind = (MetadataImageKind)reader.ReadInt32(); - Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - -#pragma warning disable CA2016 // https://github.com/dotnet/roslyn-analyzers/issues/4985 - pooledMetadata.Object.Add(ReadModuleMetadataFrom(reader, kind)); -#pragma warning restore CA2016 - } - - return (AssemblyMetadata.Create(pooledMetadata.Object), storages: default); - } - - Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); -#pragma warning disable CA2016 // https://github.com/dotnet/roslyn-analyzers/issues/4985 - return (ReadModuleMetadataFrom(reader, kind), storages: default); -#pragma warning restore CA2016 - } - if (metadataKind == MetadataImageKind.Assembly) { - using var pooledMetadata = Creator.CreateList(); - using var pooledStorage = Creator.CreateList(); - var count = reader.ReadInt32(); + + var allMetadata = new FixedSizeArrayBuilder(count); + var allHandles = new FixedSizeArrayBuilder(count); + for (var i = 0; i < count; i++) { metadataKind = (MetadataImageKind)reader.ReadInt32(); Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - var (metadata, storage) = ReadModuleMetadataFrom(reader, kind, cancellationToken); + var (metadata, storageHandle) = ReadModuleMetadataFrom(reader, kind, cancellationToken); - pooledMetadata.Object.Add(metadata); - pooledStorage.Object.Add(storage); + allMetadata.Add(metadata); + allHandles.Add(storageHandle); } - return (AssemblyMetadata.Create(pooledMetadata.Object), pooledStorage.Object.ToImmutableArrayOrEmpty()); + return (AssemblyMetadata.Create(allMetadata.MoveToImmutable()), allHandles.MoveToImmutable()); } + else + { + Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - Contract.ThrowIfFalse(metadataKind == MetadataImageKind.Module); - - var moduleInfo = ReadModuleMetadataFrom(reader, kind, cancellationToken); - return (moduleInfo.metadata, ImmutableArray.Create(moduleInfo.storage)); + var moduleInfo = ReadModuleMetadataFrom(reader, kind, cancellationToken); + return (moduleInfo.metadata, [moduleInfo.storageHandle]); + } } - private (ModuleMetadata metadata, ITemporaryStreamStorageInternal storage) ReadModuleMetadataFrom( + private (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFrom( ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - var (storage, length) = GetTemporaryStorage(reader, kind, cancellationToken); - - var storageStream = storage.ReadStream(cancellationToken); - Contract.ThrowIfFalse(length == storageStream.Length); - - GetMetadata(storageStream, length, out var metadata, out var lifeTimeObject); - - // make sure we keep storageStream alive while Metadata is alive - // we use conditional weak table since we can't control metadata liftetime - if (lifeTimeObject != null) - s_lifetimeMap.Add(metadata, lifeTimeObject); - - return (metadata, storage); - } - - private static ModuleMetadata ReadModuleMetadataFrom(ObjectReader reader, SerializationKinds kind) - { - Contract.ThrowIfFalse(SerializationKinds.Bits == kind); - - var array = reader.ReadByteArray(); - var pinnedObject = new PinnedObject(array); - - var metadata = ModuleMetadata.CreateFromMetadata(pinnedObject.GetPointer(), array.Length); + Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile); - // make sure we keep storageStream alive while Metadata is alive - // we use conditional weak table since we can't control metadata liftetime - s_lifetimeMap.Add(metadata, pinnedObject); + return kind == SerializationKinds.Bits + ? ReadModuleMetadataFromBits() + : ReadModuleMetadataFromMemoryMappedFile(); - return metadata; - } - - private (ITemporaryStreamStorageInternal storage, long length) GetTemporaryStorage( - ObjectReader reader, SerializationKinds kind, CancellationToken cancellationToken) - { - Contract.ThrowIfFalse(kind is SerializationKinds.Bits or SerializationKinds.MemoryMapFile); + (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromMemoryMappedFile() + { + // Host passed us a segment of its own memory mapped file. We can just refer to that segment directly as it + // will not be released by the host. + var storageIdentifier = TemporaryStorageIdentifier.ReadFrom(reader); + var storageHandle = TemporaryStorageService.GetStreamHandle(storageIdentifier); + return ReadModuleMetadataFromStorage(storageHandle); + } - if (kind == SerializationKinds.Bits) + (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromBits() { - var storage = _storageService.CreateTemporaryStreamStorage(); + // Host is sending us all the data as bytes. Take that and write that out to a memory mapped file on the + // server side so that we can refer to this data uniformly. using var stream = SerializableBytes.CreateWritableStream(); - CopyByteArrayToStream(reader, stream, cancellationToken); var length = stream.Length; - - stream.Position = 0; - storage.WriteStream(stream, cancellationToken); - - return (storage, length); + var storageHandle = _storageService.WriteToTemporaryStorage(stream, cancellationToken); + Contract.ThrowIfTrue(length != storageHandle.Identifier.Size); + return ReadModuleMetadataFromStorage(storageHandle); } - else - { - var service2 = (TemporaryStorageService)_storageService; - var name = reader.ReadRequiredString(); - var offset = reader.ReadInt64(); - var size = reader.ReadInt64(); - -#pragma warning disable CA1416 // Validate platform compatibility - var storage = service2.AttachTemporaryStreamStorage(name, offset, size); -#pragma warning restore CA1416 // Validate platform compatibility - var length = size; - - return (storage, length); - } - } - - private static void GetMetadata(Stream stream, long length, out ModuleMetadata metadata, out object? lifeTimeObject) - { - if (stream is UnmanagedMemoryStream unmanagedStream) + (ModuleMetadata metadata, TemporaryStorageStreamHandle storageHandle) ReadModuleMetadataFromStorage( + TemporaryStorageStreamHandle storageHandle) { - // For an unmanaged memory stream, ModuleMetadata can take ownership directly. + // Now read in the module data using that identifier. This will either be reading from the host's memory if + // they passed us the information about that memory segment. Or it will be reading from our own memory if they + // sent us the full contents. + var unmanagedStream = storageHandle.ReadFromTemporaryStorage(cancellationToken); + Contract.ThrowIfFalse(storageHandle.Identifier.Size == unmanagedStream.Length); + + // For an unmanaged memory stream, ModuleMetadata can take ownership directly. Stream will be kept alive as + // long as the ModuleMetadata is alive due to passing its .Dispose method in as the onDispose callback of + // the metadata. unsafe { - metadata = ModuleMetadata.CreateFromMetadata( + var metadata = ModuleMetadata.CreateFromMetadata( (IntPtr)unmanagedStream.PositionPointer, (int)unmanagedStream.Length, unmanagedStream.Dispose); - lifeTimeObject = null; - return; + return (metadata, storageHandle); } } - - PinnedObject pinnedObject; - if (stream is MemoryStream memory && - memory.TryGetBuffer(out var buffer) && - buffer.Offset == 0) - { - pinnedObject = new PinnedObject(buffer.Array!); - } - else - { - var array = new byte[length]; - stream.Read(array, 0, (int)length); - pinnedObject = new PinnedObject(array); - } - - metadata = ModuleMetadata.CreateFromMetadata(pinnedObject.GetPointer(), (int)length); - lifeTimeObject = pinnedObject; } private static void CopyByteArrayToStream(ObjectReader reader, Stream stream, CancellationToken cancellationToken) @@ -563,35 +467,6 @@ private static void WriteUnresolvedAnalyzerReferenceTo(AnalyzerReference referen } } - private sealed class PinnedObject : IDisposable - { - // shouldn't be read-only since GCHandle is a mutable struct - private GCHandle _gcHandle; - - public PinnedObject(byte[] array) - => _gcHandle = GCHandle.Alloc(array, GCHandleType.Pinned); - - internal IntPtr GetPointer() - => _gcHandle.AddrOfPinnedObject(); - - private void OnDispose() - { - if (_gcHandle.IsAllocated) - { - _gcHandle.Free(); - } - } - - ~PinnedObject() - => OnDispose(); - - public void Dispose() - { - GC.SuppressFinalize(this); - OnDispose(); - } - } - private sealed class MissingMetadataReference : PortableExecutableReference { private readonly DocumentationProvider _provider; @@ -629,16 +504,22 @@ protected override PortableExecutableReference WithPropertiesImpl(MetadataRefere private sealed class SerializedMetadataReference : PortableExecutableReference, ISupportTemporaryStorage { private readonly Metadata _metadata; - private readonly ImmutableArray _storagesOpt; + private readonly ImmutableArray _storageHandles; private readonly DocumentationProvider _provider; + public IReadOnlyList StorageHandles => _storageHandles; + public SerializedMetadataReference( - MetadataReferenceProperties properties, string? fullPath, - Metadata metadata, ImmutableArray storagesOpt, DocumentationProvider initialDocumentation) + MetadataReferenceProperties properties, + string? fullPath, + Metadata metadata, + ImmutableArray storageHandles, + DocumentationProvider initialDocumentation) : base(properties, fullPath, initialDocumentation) { + Contract.ThrowIfTrue(storageHandles.IsDefault); _metadata = metadata; - _storagesOpt = storagesOpt; + _storageHandles = storageHandles; _provider = initialDocumentation; } @@ -653,9 +534,6 @@ protected override Metadata GetMetadataImpl() => _metadata; protected override PortableExecutableReference WithPropertiesImpl(MetadataReferenceProperties properties) - => new SerializedMetadataReference(properties, FilePath, _metadata, _storagesOpt, _provider); - - public IReadOnlyList? GetStorages() - => _storagesOpt.IsDefault ? null : _storagesOpt; + => new SerializedMetadataReference(properties, FilePath, _metadata, _storageHandles, _provider); } } diff --git a/src/Workspaces/Core/Portable/Serialization/SolutionReplicationContext.cs b/src/Workspaces/Core/Portable/Serialization/SolutionReplicationContext.cs deleted file mode 100644 index e1cb61be7218f..0000000000000 --- a/src/Workspaces/Core/Portable/Serialization/SolutionReplicationContext.cs +++ /dev/null @@ -1,31 +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 Microsoft.CodeAnalysis.PooledObjects; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Serialization; - -internal readonly struct SolutionReplicationContext : IDisposable -{ - private static readonly ObjectPool> s_pool = new(() => []); - - private readonly ConcurrentSet _resources; - - public SolutionReplicationContext() - => _resources = s_pool.Allocate(); - - public void AddResource(IDisposable resource) - => _resources.Add(resource); - - public void Dispose() - { - // TODO: https://github.com/dotnet/roslyn/issues/49973 - // Currently we don't dispose resources, only keep them alive. - // Shouldn't we dispose them? - // _resources.All(resource => resource.Dispose()); - s_pool.ClearAndFree(_resources); - } -} diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs index bf841432cddf8..aba239ec1cb92 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ISymbolExtensions.cs @@ -344,7 +344,7 @@ private static XNode[] RewriteMany(ISymbol symbol, HashSet? visitedSymb result.AddRange(RewriteInheritdocElements(symbol, visitedSymbols, compilation, child, cancellationToken)); } - return result.ToArray(); + return [.. result]; } private static XNode[]? RewriteInheritdocElement(ISymbol memberSymbol, HashSet? visitedSymbols, Compilation compilation, XElement element, CancellationToken cancellationToken) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs index b45c69cfd0dc4..f929abc39b74d 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.AnonymousTypeRemover.cs @@ -47,7 +47,7 @@ public override ITypeSymbol VisitNamedType(INamedTypeSymbol symbol) if (arguments.SequenceEqual(symbol.TypeArguments)) return symbol; - return symbol.ConstructedFrom.Construct(arguments.ToArray()); + return symbol.ConstructedFrom.Construct([.. arguments]); } public override ITypeSymbol VisitPointerType(IPointerTypeSymbol symbol) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs index 24f728ff9b703..b591d1c9d2818 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnavailableTypeParameterRemover.cs @@ -45,7 +45,7 @@ public override ITypeSymbol VisitNamedType(INamedTypeSymbol symbol) return symbol; } - return symbol.ConstructedFrom.Construct(arguments.ToArray()); + return symbol.ConstructedFrom.Construct([.. arguments]); } public override ITypeSymbol VisitPointerType(IPointerTypeSymbol symbol) diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs index 31510f8004fae..93bde82d583ef 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ITypeSymbolExtensions.UnnamedErrorTypeRemover.cs @@ -49,7 +49,7 @@ public override ITypeSymbol VisitNamedType(INamedTypeSymbol symbol) return symbol; } - return symbol.ConstructedFrom.Construct(arguments.ToArray()); + return symbol.ConstructedFrom.Construct([.. arguments]); } public override ITypeSymbol VisitPointerType(IPointerTypeSymbol symbol) diff --git a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs index f9de85f7493c1..174d252614bbc 100644 --- a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs +++ b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListener.cs @@ -235,7 +235,7 @@ public ImmutableArray ActiveDiagnosticTokens return []; } - return _diagnosticTokenList.ToImmutableArray(); + return [.. _diagnosticTokenList]; } } } diff --git a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs index 2fb062340a4c3..ed5be74296ee3 100644 --- a/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs +++ b/src/Workspaces/Core/Portable/Shared/TestHooks/AsynchronousOperationListenerProvider.cs @@ -151,7 +151,7 @@ public async Task WaitAllAsync(Workspace? workspace, string[]? featureNames = nu do { // wait for all current tasks to be done for the time given - if (Task.WaitAll(tasks.ToArray(), smallTimeout)) + if (Task.WaitAll([.. tasks], smallTimeout)) { // current set of tasks are done. // see whether there are new tasks added while we were waiting diff --git a/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs b/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs index 6cc6cad8d4b1a..4d740090dc8a4 100644 --- a/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs +++ b/src/Workspaces/Core/Portable/Shared/Utilities/BloomFilter.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; namespace Microsoft.CodeAnalysis.Shared.Utilities; @@ -113,7 +114,7 @@ private static int ComputeK(int expectedCount, double falsePositiveProbability) /// /// Murmur hash is public domain. Actual code is included below as reference. /// - private int ComputeHash(string key, int seed) + private static int ComputeHash(string key, int seed, bool isCaseSensitive) { unchecked { @@ -127,8 +128,8 @@ private int ComputeHash(string key, int seed) var index = 0; while (numberOfCharsLeft >= 2) { - var c1 = GetCharacter(key, index); - var c2 = GetCharacter(key, index + 1); + var c1 = GetCharacter(key, index, isCaseSensitive); + var c2 = GetCharacter(key, index + 1, isCaseSensitive); h = CombineTwoCharacters(h, c1, c2); @@ -140,7 +141,7 @@ private int ComputeHash(string key, int seed) // odd length. if (numberOfCharsLeft == 1) { - var c = GetCharacter(key, index); + var c = GetCharacter(key, index, isCaseSensitive); h = CombineLastCharacter(h, c); } @@ -225,10 +226,10 @@ private static uint CombineTwoCharacters(uint h, uint c1, uint c2) } } - private char GetCharacter(string key, int index) + private static char GetCharacter(string key, int index, bool isCaseSensitive) { var c = key[index]; - return _isCaseSensitive ? c : char.ToLowerInvariant(c); + return isCaseSensitive ? c : char.ToLowerInvariant(c); } private static char GetCharacter(long key, int index) @@ -319,13 +320,13 @@ public void Add(string value) { for (var i = 0; i < _hashFunctionCount; i++) { - _bitArray[GetBitArrayIndex(value, i)] = true; + var hash = ComputeHash(value, i, _isCaseSensitive); + _bitArray[GetBitArrayIndexFromHash(hash)] = true; } } - private int GetBitArrayIndex(string value, int i) + private int GetBitArrayIndexFromHash(int hash) { - var hash = ComputeHash(value, i); hash %= _bitArray.Length; return Math.Abs(hash); } @@ -334,22 +335,24 @@ public void Add(long value) { for (var i = 0; i < _hashFunctionCount; i++) { - _bitArray[GetBitArrayIndex(value, i)] = true; + var hash = ComputeHash(value, i); + _bitArray[GetBitArrayIndexFromHash(hash)] = true; } } - private int GetBitArrayIndex(long value, int i) - { - var hash = ComputeHash(value, i); - hash %= _bitArray.Length; - return Math.Abs(hash); - } - public bool ProbablyContains(string value) { + // Request an array of immutable hashes for this input. Note that it's possible + // that the returned array might return a cached entry calculated by a different + // bloom filter and thus might have more entries than we need, but it's ok as + // it's guaranteed that the first _hashFunctionCount of those values are the values + // we would have computed had we not used the cache. + var hashes = BloomFilterHash.GetOrCreateHashArray(value, _isCaseSensitive, _hashFunctionCount); + for (var i = 0; i < _hashFunctionCount; i++) { - if (!_bitArray[GetBitArrayIndex(value, i)]) + var hash = hashes[i]; + if (!_bitArray[GetBitArrayIndexFromHash(hash)]) { return false; } @@ -362,7 +365,8 @@ public bool ProbablyContains(long value) { for (var i = 0; i < _hashFunctionCount; i++) { - if (!_bitArray[GetBitArrayIndex(value, i)]) + var hash = ComputeHash(value, i); + if (!_bitArray[GetBitArrayIndexFromHash(hash)]) { return false; } @@ -395,4 +399,86 @@ private static bool IsEquivalent(BitArray array1, BitArray array2) return true; } + + /// + /// Provides mechanism to efficiently obtain bloom filter hash for a value. Backed by a single element cache. + /// + internal sealed class BloomFilterHash + { + private static BloomFilterHash? s_cachedHash; + + private readonly string _value; + private readonly bool _isCaseSensitive; + private readonly ImmutableArray _hashes; + + private BloomFilterHash(string value, bool isCaseSensitive, int hashFunctionCount) + { + _value = value; + _isCaseSensitive = isCaseSensitive; + + var hashBuilder = new FixedSizeArrayBuilder(hashFunctionCount); + + for (var i = 0; i < hashFunctionCount; i++) + hashBuilder.Add(BloomFilter.ComputeHash(value, i, _isCaseSensitive)); + + _hashes = hashBuilder.MoveToImmutable(); + } + + /// + /// Although calculating this hash isn't terribly expensive, it does involve multiple + /// (usually around 13) hashings of the string (the actual count is ). + /// The typical usage pattern of bloom filters is that some operation (eg: find references) + /// requires asking a multitude of bloom filters whether a particular value is likely contained. + /// The vast majority of those bloom filters will end up hashing that string to the same values, so + /// we put those values into a simple cache and see if it can be used before calculating. + /// Local testing has put the hit rate of this at around 99%. + /// + /// Note that it's possible for this method to return an array from the cache longer than hashFunctionCount, + /// but if so, it's guaranteed that the values returned in the first hashFunctionCount entries are + /// the same as if the cache hadn't been used. + /// + public static ImmutableArray GetOrCreateHashArray(string value, bool isCaseSensitive, int hashFunctionCount) + { + var cachedHash = s_cachedHash; + + // Not an equivalency check on the hashFunctionCount as a longer array is ok. This is because the + // values in the array are determined by value and isCaseSensitive and hashFunctionCount is simply + // used to determine the length of the returned array. As long as the cached entry matches the value + // and isCaseSensitive and is at least as long as we need, then we can use it. + if (cachedHash == null + || cachedHash._isCaseSensitive != isCaseSensitive + || cachedHash._hashes.Length < hashFunctionCount + || cachedHash._value != value) + { + cachedHash = new BloomFilterHash(value, isCaseSensitive, hashFunctionCount); + s_cachedHash = cachedHash; + } + + return cachedHash._hashes; + } + + // Used only by tests + internal static bool TryGetCachedEntry(out bool isCaseSensitive, out string value) + { + var cachedHash = s_cachedHash; + + if (cachedHash == null) + { + isCaseSensitive = false; + value = string.Empty; + + return false; + } + + isCaseSensitive = cachedHash._isCaseSensitive; + value = cachedHash._value; + + return true; + } + + internal static void ResetCachedEntry() + { + s_cachedHash = null; + } + } } diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.DirectMemoryAccessStreamReader.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.DirectMemoryAccessStreamReader.cs new file mode 100644 index 0000000000000..9918414654ea1 --- /dev/null +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.DirectMemoryAccessStreamReader.cs @@ -0,0 +1,76 @@ +// 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.Runtime.InteropServices; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +internal sealed partial class TemporaryStorageService +{ + private unsafe class DirectMemoryAccessStreamReader : TextReaderWithLength + { + private char* _position; + private readonly char* _end; + + public DirectMemoryAccessStreamReader(char* src, int length) + : base(length) + { + RoslynDebug.Assert(src != null); + RoslynDebug.Assert(length >= 0); + + _position = src; + _end = _position + length; + } + + public override int Peek() + { + if (_position >= _end) + { + return -1; + } + + return *_position; + } + + public override int Read() + { + if (_position >= _end) + { + return -1; + } + + return *_position++; + } + + public override int Read(char[] buffer, int index, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (index < 0 || index >= buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (count < 0 || (index + count) > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(count)); + } + + count = Math.Min(count, (int)(_end - _position)); + if (count > 0) + { + Marshal.Copy((IntPtr)_position, buffer, index, count); + _position += count; + } + + return count; + } + } +} diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs index 0bd1fd88552e7..7f6b5d80dc9e2 100644 --- a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.MemoryMappedInfo.cs @@ -7,7 +7,6 @@ using System.IO; using System.IO.MemoryMappedFiles; using System.Runtime; -using System.Runtime.InteropServices; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Host; @@ -22,49 +21,35 @@ internal partial class TemporaryStorageService /// metadata dll shadow copy. shared view will help those cases. /// /// - /// Instances of this class should be disposed when they are no longer needed. After disposing this - /// instance, it should no longer be used. However, streams obtained through - /// or will not be invalidated until they are disposed independently (which - /// may occur before or after the is disposed. - /// - /// This class and its nested types have familiar APIs and predictable behavior when used in other code, - /// but are non-trivial to work on. The implementations of adhere to the best - /// practices described in - /// DG - /// Update: Dispose, Finalization, and Resource Management. Additional notes regarding operating system - /// behavior leveraged for efficiency are given in comments. + /// This class and its nested types have familiar APIs and predictable behavior when used in other code, but + /// are non-trivial to work on. /// - internal sealed class MemoryMappedInfo(ReferenceCountedDisposable memoryMappedFile, string name, long offset, long size) : IDisposable + internal sealed class MemoryMappedInfo(MemoryMappedFile memoryMappedFile, string name, long offset, long size) { /// /// The memory mapped file. /// - /// - /// It is possible for the file to be disposed prior to the view and/or the streams which use it. - /// However, the operating system does not actually close the views which are in use until the file handles - /// are closed as well, even if the file is disposed first. - /// - private readonly ReferenceCountedDisposable _memoryMappedFile = memoryMappedFile; + public readonly MemoryMappedFile MemoryMappedFile = memoryMappedFile; /// /// A weak reference to a read-only view for the memory mapped file. /// /// - /// This holds a weak counted reference to current , which - /// allows additional accessors for the same address space to be obtained up until the point when no - /// external code is using it. When the memory is no longer being used by any objects, the view of the memory mapped file is unmapped, - /// making the process address space it previously claimed available for other purposes. If/when it is - /// needed again, a new view is created. + /// This holds a weak counted reference to current , which allows + /// additional accessors for the same address space to be obtained up until the point when no external code is + /// using it. When the memory is no longer being used by any + /// objects, the view of the memory mapped file is unmapped, making the process address space it previously + /// claimed available for other purposes. If/when it is needed again, a new view is created. /// /// This view is read-only, so it is only used by . /// private ReferenceCountedDisposable.WeakReference _weakReadAccessor; - public MemoryMappedInfo(string name, long offset, long size) - : this(new ReferenceCountedDisposable(MemoryMappedFile.OpenExisting(name)), name, offset, size) - { - } + public static MemoryMappedInfo OpenExisting(string name, long offset, long size) + => new(MemoryMappedFile.OpenExisting(name), name, offset, size); + + public static MemoryMappedInfo CreateNew(string name, long size) + => new(MemoryMappedFile.CreateNew(name, size), name, offset: 0, size); /// /// The name of the memory mapped file. @@ -89,21 +74,14 @@ public MemoryMappedInfo(string name, long offset, long size) /// public UnmanagedMemoryStream CreateReadableStream() { - // Note: TryAddReference behaves according to its documentation even if the target object has been - // disposed. If it returns non-null, then the object will not be disposed before the returned - // reference is disposed (see comments on _memoryMappedFile and TryAddReference). + // Note: TryAddReference behaves according to its documentation even if the target object has been disposed. + // If it returns non-null, then the object will not be disposed before the returned reference is disposed + // (see comments on _memoryMappedFile and TryAddReference). var streamAccessor = _weakReadAccessor.TryAddReference(); if (streamAccessor == null) { var rawAccessor = RunWithCompactingGCFallback( - static info => - { - using var memoryMappedFile = info._memoryMappedFile.TryAddReference(); - if (memoryMappedFile is null) - throw new ObjectDisposedException(typeof(MemoryMappedInfo).FullName); - - return memoryMappedFile.Target.CreateViewAccessor(info.Offset, info.Size, MemoryMappedFileAccess.Read); - }, + static info => info.MemoryMappedFile.CreateViewAccessor(info.Offset, info.Size, MemoryMappedFileAccess.Read), this); streamAccessor = new ReferenceCountedDisposable(rawAccessor); _weakReadAccessor = new ReferenceCountedDisposable.WeakReference(streamAccessor); @@ -120,28 +98,20 @@ public UnmanagedMemoryStream CreateReadableStream() public Stream CreateWritableStream() { return RunWithCompactingGCFallback( - static info => - { - using var memoryMappedFile = info._memoryMappedFile.TryAddReference(); - if (memoryMappedFile is null) - throw new ObjectDisposedException(typeof(MemoryMappedInfo).FullName); - - return memoryMappedFile.Target.CreateViewStream(info.Offset, info.Size, MemoryMappedFileAccess.Write); - }, + static info => info.MemoryMappedFile.CreateViewStream(info.Offset, info.Size, MemoryMappedFileAccess.Write), this); } /// - /// Run a function which may fail with an if not enough memory is available to - /// satisfy the request. In this case, a full compacting GC pass is forced and the function is attempted - /// again. + /// Run a function which may fail with an if not enough memory is available to satisfy + /// the request. In this case, a full compacting GC pass is forced and the function is attempted again. /// /// - /// and - /// will use a native - /// memory map, which can't trigger a GC. In this case, we'd otherwise crash with OOM, so we don't care - /// about creating a UI delay with a full forced compacting GC. If it crashes the second try, it means we're - /// legitimately out of resources. + /// and will use a native memory map, + /// which can't trigger a GC. In this case, we'd otherwise crash with OOM, so we don't care about creating a UI + /// delay with a full forced compacting GC. If it crashes the second try, it means we're legitimately out of + /// resources. /// /// The type of argument to pass to the callback. /// The type returned by the function. @@ -171,42 +141,18 @@ private static void ForceCompactingGC() GC.Collect(); } - public void Dispose() - { - // See remarks on field for relation between _memoryMappedFile and the views/streams. There is no - // need to write _weakReadAccessor here since lifetime of the target is not owned by this instance. - _memoryMappedFile.Dispose(); - } - - private sealed unsafe class MemoryMappedViewUnmanagedMemoryStream : UnmanagedMemoryStream + private sealed unsafe class MemoryMappedViewUnmanagedMemoryStream( + ReferenceCountedDisposable accessor, + long length) : UnmanagedMemoryStream( + (byte*)accessor.Target.SafeMemoryMappedViewHandle.DangerousGetHandle() + accessor.Target.PointerOffset, + length) { - private readonly ReferenceCountedDisposable _accessor; - private byte* _start; - - public MemoryMappedViewUnmanagedMemoryStream(ReferenceCountedDisposable accessor, long length) - : base((byte*)accessor.Target.SafeMemoryMappedViewHandle.DangerousGetHandle() + accessor.Target.PointerOffset, length) - { - _accessor = accessor; - _start = this.PositionPointer; - } - protected override void Dispose(bool disposing) { base.Dispose(disposing); - if (disposing) - { - _accessor.Dispose(); - } - - _start = null; + accessor.Dispose(); } - - /// - /// Get underlying native memory directly. - /// - public IntPtr GetPointer() - => (IntPtr)_start; } } } diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.TemporaryStorageStreamHandle.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.TemporaryStorageStreamHandle.cs new file mode 100644 index 0000000000000..7b06db02ddbff --- /dev/null +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.TemporaryStorageStreamHandle.cs @@ -0,0 +1,35 @@ +// 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.IO; +using System.IO.MemoryMappedFiles; +using System.Threading; +using Microsoft.CodeAnalysis.Internal.Log; + +namespace Microsoft.CodeAnalysis.Host; + +internal sealed partial class TemporaryStorageService +{ + public sealed class TemporaryStorageStreamHandle( + MemoryMappedFile memoryMappedFile, + TemporaryStorageIdentifier identifier) + : ITemporaryStorageStreamHandle + { + public TemporaryStorageIdentifier Identifier => identifier; + + Stream ITemporaryStorageStreamHandle.ReadFromTemporaryStorage(CancellationToken cancellationToken) + => ReadFromTemporaryStorage(cancellationToken); + + public UnmanagedMemoryStream ReadFromTemporaryStorage(CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadStream, cancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + + var info = new MemoryMappedInfo(memoryMappedFile, Identifier.Name, Identifier.Offset, Identifier.Size); + return info.CreateReadableStream(); + } + } + } +} diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.cs new file mode 100644 index 0000000000000..7a8431f550016 --- /dev/null +++ b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageService.cs @@ -0,0 +1,294 @@ +// 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.Diagnostics; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Internal.Log; +using Microsoft.CodeAnalysis.Shared.Utilities; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +/// +/// Temporarily stores text and streams in memory mapped files. +/// +#if NETCOREAPP +[SupportedOSPlatform("windows")] +#endif +internal sealed partial class TemporaryStorageService : ITemporaryStorageServiceInternal +{ + /// + /// The maximum size in bytes of a single storage unit in a memory mapped file which is shared with other storage + /// units. + /// + /// + /// The value of 256k reduced the number of files dumped to separate memory mapped files by 60% compared to + /// the next lower power-of-2 size for Roslyn.sln itself. + /// + /// + private const long SingleFileThreshold = 256 * 1024; + + /// + /// The size in bytes of a memory mapped file created to store multiple temporary objects. + /// + /// + /// This value (8mb) creates roughly 35 memory mapped files (around 300MB) to store the contents of all of + /// Roslyn.sln a snapshot. This keeps the data safe, so that we can drop it from memory when not needed, but + /// reconstitute the contents we originally had in the snapshot in case the original files change on disk. + /// + /// + private const long MultiFileBlockSize = SingleFileThreshold * 32; + + private readonly IWorkspaceThreadingService? _workspaceThreadingService; + private readonly ITextFactoryService _textFactory; + + /// + /// The synchronization object for accessing the memory mapped file related fields (indicated in the remarks + /// of each field). + /// + /// + /// PERF DEV NOTE: A concurrent (but complex) implementation of this type with identical semantics is + /// available in source control history. The use of exclusive locks was not causing any measurable + /// performance overhead even on 28-thread machines at the time this was written. + /// + private readonly object _gate = new(); + + /// + /// The most recent memory mapped file for creating multiple storage units. It will be used via bump-pointer + /// allocation until space is no longer available in it. Access should be synchronized on + /// + private MemoryMappedFile? _fileReference; + + /// The name of the current memory mapped file for multiple storage units. Access should be synchronized on + /// + /// + private string? _name; + + /// The total size of the current memory mapped file for multiple storage units. Access should be + /// synchronized on + /// + private long _fileSize; + + /// + /// The offset into the current memory mapped file where the next storage unit can be held. Access should be + /// synchronized on . + /// + /// + private long _offset; + + [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] + private TemporaryStorageService(IWorkspaceThreadingService? workspaceThreadingService, ITextFactoryService textFactory) + { + _workspaceThreadingService = workspaceThreadingService; + _textFactory = textFactory; + } + + ITemporaryStorageTextHandle ITemporaryStorageServiceInternal.WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken) + => WriteToTemporaryStorage(text, cancellationToken); + + async Task ITemporaryStorageServiceInternal.WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken) + => await WriteToTemporaryStorageAsync(text, cancellationToken).ConfigureAwait(false); + + public TemporaryStorageTextHandle WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken) + { + var memoryMappedInfo = WriteToMemoryMappedFile(); + var identifier = new TemporaryStorageIdentifier(memoryMappedInfo.Name, memoryMappedInfo.Offset, memoryMappedInfo.Size); + return new(this, memoryMappedInfo.MemoryMappedFile, identifier, text.ChecksumAlgorithm, text.Encoding, text.GetContentHash()); + + MemoryMappedInfo WriteToMemoryMappedFile() + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteText, cancellationToken)) + { + // the method we use to get text out of SourceText uses Unicode (2bytes per char). + var size = Encoding.Unicode.GetMaxByteCount(text.Length); + var memoryMappedInfo = this.CreateTemporaryStorage(size); + + // Write the source text out as Unicode. We expect that to be cheap. + using var stream = memoryMappedInfo.CreateWritableStream(); + { + using var writer = new StreamWriter(stream, Encoding.Unicode); + text.Write(writer, cancellationToken); + } + + return memoryMappedInfo; + } + } + } + + public async Task WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken) + { + if (this._workspaceThreadingService is { IsOnMainThread: true }) + { + await Task.Yield().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + + return WriteToTemporaryStorage(text, cancellationToken); + } + + ITemporaryStorageStreamHandle ITemporaryStorageServiceInternal.WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken) + => WriteToTemporaryStorage(stream, cancellationToken); + + public TemporaryStorageStreamHandle WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken) + { + stream.Position = 0; + var memoryMappedInfo = WriteToMemoryMappedFile(); + var identifier = new TemporaryStorageIdentifier(memoryMappedInfo.Name, memoryMappedInfo.Offset, memoryMappedInfo.Size); + return new(memoryMappedInfo.MemoryMappedFile, identifier); + + MemoryMappedInfo WriteToMemoryMappedFile() + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, cancellationToken)) + { + var size = stream.Length; + var memoryMappedInfo = this.CreateTemporaryStorage(size); + using var viewStream = memoryMappedInfo.CreateWritableStream(); + { + using var pooledObject = SharedPools.ByteArray.GetPooledObject(); + var buffer = pooledObject.Object; + while (true) + { + var count = stream.Read(buffer, 0, buffer.Length); + if (count == 0) + break; + + viewStream.Write(buffer, 0, count); + } + } + + return memoryMappedInfo; + } + } + } + + internal static TemporaryStorageStreamHandle GetStreamHandle(TemporaryStorageIdentifier storageIdentifier) + { + var memoryMappedFile = MemoryMappedFile.OpenExisting(storageIdentifier.Name); + return new(memoryMappedFile, storageIdentifier); + } + + internal TemporaryStorageTextHandle GetTextHandle( + TemporaryStorageIdentifier storageIdentifier, + SourceHashAlgorithm checksumAlgorithm, + Encoding? encoding, + ImmutableArray contentHash) + { + var memoryMappedFile = MemoryMappedFile.OpenExisting(storageIdentifier.Name); + return new(this, memoryMappedFile, storageIdentifier, checksumAlgorithm, encoding, contentHash); + } + + /// + /// Allocate shared storage of a specified size. + /// + /// + /// "Small" requests are fulfilled from oversized memory mapped files which support several individual + /// storage units. Larger requests are allocated in their own memory mapped files. + /// + /// The size of the shared storage block to allocate. + /// A describing the allocated block. + private MemoryMappedInfo CreateTemporaryStorage(long size) + { + // Larger blocks are allocated separately + if (size >= SingleFileThreshold) + return MemoryMappedInfo.CreateNew(CreateUniqueName(size), size: size); + + lock (_gate) + { + // Obtain a reference to the memory mapped file, creating one if necessary. If a reference counted + // handle to a memory mapped file is obtained in this section, it must either be disposed before + // returning or returned to the caller who will own it through the MemoryMappedInfo. + var reference = _fileReference; + if (reference == null || _offset + size > _fileSize) + { + var mapName = CreateUniqueName(MultiFileBlockSize); + + reference = MemoryMappedFile.CreateNew(mapName, MultiFileBlockSize); + _fileReference = reference; + _name = mapName; + _fileSize = MultiFileBlockSize; + _offset = size; + return new MemoryMappedInfo(reference, _name, offset: 0, size: size); + } + else + { + // Reserve additional space in the existing storage location + Contract.ThrowIfNull(_name); + _offset += size; + return new MemoryMappedInfo(reference, _name, _offset - size, size); + } + } + } + + public static string CreateUniqueName(long size) + => "Roslyn Temp Storage " + size.ToString() + " " + Guid.NewGuid().ToString("N"); + + public sealed class TemporaryStorageTextHandle( + TemporaryStorageService storageService, + MemoryMappedFile memoryMappedFile, + TemporaryStorageIdentifier identifier, + SourceHashAlgorithm checksumAlgorithm, + Encoding? encoding, + ImmutableArray contentHash) + : ITemporaryStorageTextHandle + { + public TemporaryStorageIdentifier Identifier => identifier; + public SourceHashAlgorithm ChecksumAlgorithm => checksumAlgorithm; + public Encoding? Encoding => encoding; + public ImmutableArray ContentHash => contentHash; + + public async Task ReadFromTemporaryStorageAsync(CancellationToken cancellationToken) + { + // There is a reason for implementing it like this: proper async implementation + // that reads the underlying memory mapped file stream in an asynchronous fashion + // doesn't actually work. Windows doesn't offer + // any non-blocking way to read from a memory mapped file; the underlying memcpy + // may block as the memory pages back in and that's something you have to live + // with. Therefore, any implementation that attempts to use async will still + // always be blocking at least one threadpool thread in the memcpy in the case + // of a page fault. Therefore, if we're going to be blocking a thread, we should + // just block one thread and do the whole thing at once vs. a fake "async" + // implementation which will continue to requeue work back to the thread pool. + if (storageService._workspaceThreadingService is { IsOnMainThread: true }) + { + await Task.Yield().ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + + return ReadFromTemporaryStorage(cancellationToken); + } + + public SourceText ReadFromTemporaryStorage(CancellationToken cancellationToken) + { + using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadText, cancellationToken)) + { + var info = new MemoryMappedInfo(memoryMappedFile, Identifier.Name, Identifier.Offset, Identifier.Size); + using var stream = info.CreateReadableStream(); + using var reader = CreateTextReaderFromTemporaryStorage(stream); + + // we pass in encoding we got from original source text even if it is null. + return storageService._textFactory.CreateText(reader, encoding, checksumAlgorithm, cancellationToken); + } + } + + private static unsafe DirectMemoryAccessStreamReader CreateTextReaderFromTemporaryStorage(UnmanagedMemoryStream stream) + { + var src = (char*)stream.PositionPointer; + + // BOM: Unicode, little endian + // Skip the BOM when creating the reader + Debug.Assert(*src == 0xFEFF); + + return new DirectMemoryAccessStreamReader(src + 1, (int)stream.Length / sizeof(char) - 1); + } + } +} diff --git a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageServiceFactory.cs b/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageServiceFactory.cs deleted file mode 100644 index 451e15a091a56..0000000000000 --- a/src/Workspaces/Core/Portable/TemporaryStorage/TemporaryStorageServiceFactory.cs +++ /dev/null @@ -1,490 +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.Diagnostics; -using System.IO; -using System.IO.MemoryMappedFiles; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.Internal.Log; -using Microsoft.CodeAnalysis.Shared.Utilities; -using Microsoft.CodeAnalysis.Text; -using Roslyn.Utilities; - -namespace Microsoft.CodeAnalysis.Host; - -/// -/// Temporarily stores text and streams in memory mapped files. -/// -#if NETCOREAPP -[SupportedOSPlatform("windows")] -#endif -internal sealed partial class TemporaryStorageService : ITemporaryStorageServiceInternal -{ - /// - /// The maximum size in bytes of a single storage unit in a memory mapped file which is shared with other - /// storage units. - /// - /// - /// This value was arbitrarily chosen and appears to work well. Can be changed if data suggests - /// something better. - /// - /// - private const long SingleFileThreshold = 128 * 1024; - - /// - /// The size in bytes of a memory mapped file created to store multiple temporary objects. - /// - /// - /// This value was arbitrarily chosen and appears to work well. Can be changed if data suggests - /// something better. - /// - /// - private const long MultiFileBlockSize = SingleFileThreshold * 32; - - private readonly IWorkspaceThreadingService? _workspaceThreadingService; - private readonly ITextFactoryService _textFactory; - - /// - /// The synchronization object for accessing the memory mapped file related fields (indicated in the remarks - /// of each field). - /// - /// - /// PERF DEV NOTE: A concurrent (but complex) implementation of this type with identical semantics is - /// available in source control history. The use of exclusive locks was not causing any measurable - /// performance overhead even on 28-thread machines at the time this was written. - /// - private readonly object _gate = new(); - - /// - /// The most recent memory mapped file for creating multiple storage units. It will be used via bump-pointer - /// allocation until space is no longer available in it. - /// - /// - /// Access should be synchronized on . - /// - private ReferenceCountedDisposable.WeakReference _weakFileReference; - - /// The name of the current memory mapped file for multiple storage units. - /// - /// Access should be synchronized on . - /// - /// - private string? _name; - - /// The total size of the current memory mapped file for multiple storage units. - /// - /// Access should be synchronized on . - /// - /// - private long _fileSize; - - /// - /// The offset into the current memory mapped file where the next storage unit can be held. - /// - /// - /// Access should be synchronized on . - /// - /// - private long _offset; - - [Obsolete(MefConstruction.FactoryMethodMessage, error: true)] - private TemporaryStorageService(IWorkspaceThreadingService? workspaceThreadingService, ITextFactoryService textFactory) - { - _workspaceThreadingService = workspaceThreadingService; - _textFactory = textFactory; - } - - public ITemporaryTextStorageInternal CreateTemporaryTextStorage() - => new TemporaryTextStorage(this); - - public TemporaryTextStorage AttachTemporaryTextStorage( - string storageName, long offset, long size, SourceHashAlgorithm checksumAlgorithm, Encoding? encoding, ImmutableArray contentHash) - => new(this, storageName, offset, size, checksumAlgorithm, encoding, contentHash); - - ITemporaryStreamStorageInternal ITemporaryStorageServiceInternal.CreateTemporaryStreamStorage() - => CreateTemporaryStreamStorage(); - - internal TemporaryStreamStorage CreateTemporaryStreamStorage() - => new(this); - - public TemporaryStreamStorage AttachTemporaryStreamStorage(string storageName, long offset, long size) - => new(this, storageName, offset, size); - - /// - /// Allocate shared storage of a specified size. - /// - /// - /// "Small" requests are fulfilled from oversized memory mapped files which support several individual - /// storage units. Larger requests are allocated in their own memory mapped files. - /// - /// The size of the shared storage block to allocate. - /// A describing the allocated block. - private MemoryMappedInfo CreateTemporaryStorage(long size) - { - if (size >= SingleFileThreshold) - { - // Larger blocks are allocated separately - var mapName = CreateUniqueName(size); - var storage = MemoryMappedFile.CreateNew(mapName, size); - return new MemoryMappedInfo(new ReferenceCountedDisposable(storage), mapName, offset: 0, size: size); - } - - lock (_gate) - { - // Obtain a reference to the memory mapped file, creating one if necessary. If a reference counted - // handle to a memory mapped file is obtained in this section, it must either be disposed before - // returning or returned to the caller who will own it through the MemoryMappedInfo. - var reference = _weakFileReference.TryAddReference(); - if (reference == null || _offset + size > _fileSize) - { - var mapName = CreateUniqueName(MultiFileBlockSize); - var file = MemoryMappedFile.CreateNew(mapName, MultiFileBlockSize); - - reference = new ReferenceCountedDisposable(file); - _weakFileReference = new ReferenceCountedDisposable.WeakReference(reference); - _name = mapName; - _fileSize = MultiFileBlockSize; - _offset = size; - return new MemoryMappedInfo(reference, _name, offset: 0, size: size); - } - else - { - // Reserve additional space in the existing storage location - Contract.ThrowIfNull(_name); - _offset += size; - return new MemoryMappedInfo(reference, _name, _offset - size, size); - } - } - } - - public static string CreateUniqueName(long size) - => "Roslyn Temp Storage " + size.ToString() + " " + Guid.NewGuid().ToString("N"); - - public sealed class TemporaryTextStorage : ITemporaryTextStorageInternal, ITemporaryStorageWithName - { - private readonly TemporaryStorageService _service; - private SourceHashAlgorithm _checksumAlgorithm; - private Encoding? _encoding; - private ImmutableArray _contentHash; - private MemoryMappedInfo? _memoryMappedInfo; - - public TemporaryTextStorage(TemporaryStorageService service) - => _service = service; - - public TemporaryTextStorage( - TemporaryStorageService service, - string storageName, - long offset, - long size, - SourceHashAlgorithm checksumAlgorithm, - Encoding? encoding, - ImmutableArray contentHash) - { - _service = service; - _checksumAlgorithm = checksumAlgorithm; - _encoding = encoding; - _contentHash = contentHash; - _memoryMappedInfo = new MemoryMappedInfo(storageName, offset, size); - } - - // TODO: cleanup https://github.com/dotnet/roslyn/issues/43037 - // Offset, Size not accessed if Name is null - public string? Name => _memoryMappedInfo?.Name; - public long Offset => _memoryMappedInfo!.Offset; - public long Size => _memoryMappedInfo!.Size; - - /// - /// Gets the value for the property for the - /// represented by this temporary storage. - /// - public SourceHashAlgorithm ChecksumAlgorithm => _checksumAlgorithm; - - /// - /// Gets the value for the property for the - /// represented by this temporary storage. - /// - public Encoding? Encoding => _encoding; - - /// - /// Gets the checksum for the represented by this temporary storage. This is equivalent - /// to calling . - /// - public ImmutableArray ContentHash => _contentHash; - - public void Dispose() - { - // Destructors of SafeHandle and FileStream in MemoryMappedFile - // will eventually release resources if this Dispose is not called - // explicitly - _memoryMappedInfo?.Dispose(); - - _memoryMappedInfo = null; - _encoding = null; - _contentHash = default; - } - - public SourceText ReadText(CancellationToken cancellationToken) - { - if (_memoryMappedInfo == null) - { - throw new InvalidOperationException(); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadText, cancellationToken)) - { - using var stream = _memoryMappedInfo.CreateReadableStream(); - using var reader = CreateTextReaderFromTemporaryStorage(stream); - - // we pass in encoding we got from original source text even if it is null. - return _service._textFactory.CreateText(reader, _encoding, _checksumAlgorithm, cancellationToken); - } - } - - public async Task ReadTextAsync(CancellationToken cancellationToken) - { - // There is a reason for implementing it like this: proper async implementation - // that reads the underlying memory mapped file stream in an asynchronous fashion - // doesn't actually work. Windows doesn't offer - // any non-blocking way to read from a memory mapped file; the underlying memcpy - // may block as the memory pages back in and that's something you have to live - // with. Therefore, any implementation that attempts to use async will still - // always be blocking at least one threadpool thread in the memcpy in the case - // of a page fault. Therefore, if we're going to be blocking a thread, we should - // just block one thread and do the whole thing at once vs. a fake "async" - // implementation which will continue to requeue work back to the thread pool. - if (_service._workspaceThreadingService is { IsOnMainThread: true }) - { - await Task.Yield().ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } - - return ReadText(cancellationToken); - } - - public void WriteText(SourceText text, CancellationToken cancellationToken) - { - if (_memoryMappedInfo != null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteText, cancellationToken)) - { - _checksumAlgorithm = text.ChecksumAlgorithm; - _encoding = text.Encoding; - _contentHash = text.GetContentHash(); - - // the method we use to get text out of SourceText uses Unicode (2bytes per char). - var size = Encoding.Unicode.GetMaxByteCount(text.Length); - _memoryMappedInfo = _service.CreateTemporaryStorage(size); - - // Write the source text out as Unicode. We expect that to be cheap. - using var stream = _memoryMappedInfo.CreateWritableStream(); - using var writer = new StreamWriter(stream, Encoding.Unicode); - - text.Write(writer, cancellationToken); - } - } - - public async Task WriteTextAsync(SourceText text, CancellationToken cancellationToken) - { - // See commentary in ReadTextAsync for why this is implemented this way. - if (_service._workspaceThreadingService is { IsOnMainThread: true }) - { - await Task.Yield().ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - } - - WriteText(text, cancellationToken); - } - - private static unsafe TextReader CreateTextReaderFromTemporaryStorage(UnmanagedMemoryStream stream) - { - var src = (char*)stream.PositionPointer; - - // BOM: Unicode, little endian - // Skip the BOM when creating the reader - Debug.Assert(*src == 0xFEFF); - - return new DirectMemoryAccessStreamReader(src + 1, (int)stream.Length / sizeof(char) - 1); - } - } - - internal sealed class TemporaryStreamStorage : ITemporaryStreamStorageInternal, ITemporaryStorageWithName - { - private readonly TemporaryStorageService _service; - private MemoryMappedInfo? _memoryMappedInfo; - - public TemporaryStreamStorage(TemporaryStorageService service) - => _service = service; - - public TemporaryStreamStorage(TemporaryStorageService service, string storageName, long offset, long size) - { - _service = service; - _memoryMappedInfo = new MemoryMappedInfo(storageName, offset, size); - } - - // TODO: clean up https://github.com/dotnet/roslyn/issues/43037 - // Offset, Size is only used when Name is not null. - public string? Name => _memoryMappedInfo?.Name; - public long Offset => _memoryMappedInfo!.Offset; - public long Size => _memoryMappedInfo!.Size; - - public void Dispose() - { - // Destructors of SafeHandle and FileStream in MemoryMappedFile - // will eventually release resources if this Dispose is not called - // explicitly - _memoryMappedInfo?.Dispose(); - _memoryMappedInfo = null; - } - - Stream ITemporaryStreamStorageInternal.ReadStream(CancellationToken cancellationToken) - => ReadStream(cancellationToken); - - public UnmanagedMemoryStream ReadStream(CancellationToken cancellationToken) - { - if (_memoryMappedInfo == null) - { - throw new InvalidOperationException(); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_ReadStream, cancellationToken)) - { - cancellationToken.ThrowIfCancellationRequested(); - - return _memoryMappedInfo.CreateReadableStream(); - } - } - - public Task ReadStreamAsync(CancellationToken cancellationToken = default) - { - // See commentary in ReadTextAsync for why this is implemented this way. - return Task.Factory.StartNew(() => ReadStream(cancellationToken), cancellationToken, TaskCreationOptions.None, TaskScheduler.Default); - } - - public void WriteStream(Stream stream, CancellationToken cancellationToken = default) - { - // The Wait() here will not actually block, since with useAsync: false, the - // entire operation will already be done when WaitStreamMaybeAsync completes. - WriteStreamMaybeAsync(stream, useAsync: false, cancellationToken: cancellationToken).GetAwaiter().GetResult(); - } - - public Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken = default) - => WriteStreamMaybeAsync(stream, useAsync: true, cancellationToken: cancellationToken); - - private async Task WriteStreamMaybeAsync(Stream stream, bool useAsync, CancellationToken cancellationToken) - { - if (_memoryMappedInfo != null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - - using (Logger.LogBlock(FunctionId.TemporaryStorageServiceFactory_WriteStream, cancellationToken)) - { - var size = stream.Length; - _memoryMappedInfo = _service.CreateTemporaryStorage(size); - using var viewStream = _memoryMappedInfo.CreateWritableStream(); - - var buffer = SharedPools.ByteArray.Allocate(); - try - { - while (true) - { - int count; - if (useAsync) - { - count = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - } - else - { - count = stream.Read(buffer, 0, buffer.Length); - } - - if (count == 0) - { - break; - } - - viewStream.Write(buffer, 0, count); - } - } - finally - { - SharedPools.ByteArray.Free(buffer); - } - } - } - } -} - -internal unsafe class DirectMemoryAccessStreamReader : TextReaderWithLength -{ - private char* _position; - private readonly char* _end; - - public DirectMemoryAccessStreamReader(char* src, int length) - : base(length) - { - RoslynDebug.Assert(src != null); - RoslynDebug.Assert(length >= 0); - - _position = src; - _end = _position + length; - } - - public override int Peek() - { - if (_position >= _end) - { - return -1; - } - - return *_position; - } - - public override int Read() - { - if (_position >= _end) - { - return -1; - } - - return *_position++; - } - - public override int Read(char[] buffer, int index, int count) - { - if (buffer == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - - if (index < 0 || index >= buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(index)); - } - - if (count < 0 || (index + count) > buffer.Length) - { - throw new ArgumentOutOfRangeException(nameof(count)); - } - - count = Math.Min(count, (int)(_end - _position)); - if (count > 0) - { - Marshal.Copy((IntPtr)_position, buffer, index, count); - _position += count; - } - - return count; - } -} diff --git a/src/Features/Core/Portable/SolutionCrawler/DefaultDocumentTrackingService.cs b/src/Workspaces/Core/Portable/Workspace/DocumentTracking/DefaultDocumentTrackingService.cs similarity index 68% rename from src/Features/Core/Portable/SolutionCrawler/DefaultDocumentTrackingService.cs rename to src/Workspaces/Core/Portable/Workspace/DocumentTracking/DefaultDocumentTrackingService.cs index 0c501f07d1f4e..596cf877c78ab 100644 --- a/src/Features/Core/Portable/SolutionCrawler/DefaultDocumentTrackingService.cs +++ b/src/Workspaces/Core/Portable/Workspace/DocumentTracking/DefaultDocumentTrackingService.cs @@ -9,18 +9,11 @@ namespace Microsoft.CodeAnalysis.SolutionCrawler; -[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Default)] -[Shared] -internal sealed class DefaultDocumentTrackingService : IDocumentTrackingService +[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Default), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class DefaultDocumentTrackingService() : IDocumentTrackingService { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public DefaultDocumentTrackingService() - { - } - - public bool SupportsDocumentTracking => false; - public event EventHandler ActiveDocumentChanged { add { } remove { } } public ImmutableArray GetVisibleDocuments() diff --git a/src/Workspaces/Core/Portable/Workspace/DocumentTracking/IDocumentTrackingService.cs b/src/Workspaces/Core/Portable/Workspace/DocumentTracking/IDocumentTrackingService.cs new file mode 100644 index 0000000000000..6944f40fade72 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/DocumentTracking/IDocumentTrackingService.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis; + +/// +/// Retrieves information about what documents are currently active or visible in the host workspace. Note: this +/// information is fundamentally racy (it can change directly after it is requested), and on different threads than the +/// thread that asks for it. As such, this information must only be used to provide a hint towards how a +/// feature should go about its work, it must not impact the final results that a feature produces. For example, a +/// feature is allowed to use this information to decide what order to process documents in, to try to get more relevant +/// results to a client more quickly. However, it is not allowed to use this information to decide what results to +/// return altogether. Hosts are free to implement this service to do nothing at all, always returning empty/default +/// values for the members within. As per the above, this should never affect correctness, but it may impede a +/// feature's ability to provide results in as timely a manner as possible for a client. +/// +internal interface IDocumentTrackingService : IWorkspaceService +{ + /// + /// Get the of the active document. May be null if there is no active document, the + /// active document is not in the workspace, or if this functionality is not supported by a particular host. + /// + DocumentId? TryGetActiveDocument(); + + /// + /// Get a read only collection of the s of all the visible documents in the workspace. May + /// be empty if there are no visible documents, or if this functionality is not supported by a particular host. + /// + ImmutableArray GetVisibleDocuments(); + + /// + /// Fired when the active document changes. A host is not required to support this event, even if it implements + /// . + /// + event EventHandler ActiveDocumentChanged; +} diff --git a/src/Features/Core/Portable/SolutionCrawler/IDocumentTrackingServiceExtensions.cs b/src/Workspaces/Core/Portable/Workspace/DocumentTracking/IDocumentTrackingServiceExtensions.cs similarity index 95% rename from src/Features/Core/Portable/SolutionCrawler/IDocumentTrackingServiceExtensions.cs rename to src/Workspaces/Core/Portable/Workspace/DocumentTracking/IDocumentTrackingServiceExtensions.cs index 6d3b0cff1a372..9a231ee40abbc 100644 --- a/src/Features/Core/Portable/SolutionCrawler/IDocumentTrackingServiceExtensions.cs +++ b/src/Workspaces/Core/Portable/Workspace/DocumentTracking/IDocumentTrackingServiceExtensions.cs @@ -2,11 +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; using System.Collections.Immutable; using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis; diff --git a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs index e73c12d226a0f..823d3ae7e5cca 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/PersistentStorage/IPersistentStorageConfiguration.cs @@ -37,7 +37,7 @@ internal sealed class DefaultPersistentStorageConfiguration : IPersistentStorage /// path. For example, Base64 encoding will use / which is something that we definitely do not want /// errantly added to a path. /// - private static readonly ImmutableArray s_invalidPathChars = Path.GetInvalidPathChars().Concat('/').ToImmutableArray(); + private static readonly ImmutableArray s_invalidPathChars = [.. Path.GetInvalidPathChars(), '/']; private static readonly string s_cacheDirectory; private static readonly string s_moduleFileName; diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs index f1848b42c8618..d458ba5ca481c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorage.cs @@ -12,7 +12,7 @@ namespace Microsoft.CodeAnalysis.Host; -[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.")] +[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.", error: true)] public interface ITemporaryTextStorage : IDisposable { SourceText ReadText(CancellationToken cancellationToken = default); @@ -21,7 +21,7 @@ public interface ITemporaryTextStorage : IDisposable Task WriteTextAsync(SourceText text, CancellationToken cancellationToken = default); } -[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.")] +[Obsolete("Roslyn no longer exports a mechanism to store arbitrary data in-memory.", error: true)] public interface ITemporaryStreamStorage : IDisposable { Stream ReadStream(CancellationToken cancellationToken = default); @@ -29,22 +29,3 @@ public interface ITemporaryStreamStorage : IDisposable void WriteStream(Stream stream, CancellationToken cancellationToken = default); Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken = default); } - -/// -/// TemporaryStorage can be used to read and write text to a temporary storage location. -/// -internal interface ITemporaryTextStorageInternal : IDisposable -{ - SourceText ReadText(CancellationToken cancellationToken = default); - Task ReadTextAsync(CancellationToken cancellationToken = default); - void WriteText(SourceText text, CancellationToken cancellationToken = default); - Task WriteTextAsync(SourceText text, CancellationToken cancellationToken = default); -} - -internal interface ITemporaryStreamStorageInternal : IDisposable -{ - Stream ReadStream(CancellationToken cancellationToken = default); - Task ReadStreamAsync(CancellationToken cancellationToken = default); - void WriteStream(Stream stream, CancellationToken cancellationToken = default); - Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken = default); -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs index 694b26ba147b0..4079f19f50248 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageService.cs @@ -3,19 +3,50 @@ // See the LICENSE file in the project root for more information. using System; +using System.IO; using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Host; -[Obsolete("API is no longer available")] +[Obsolete("API is no longer available", error: true)] public interface ITemporaryStorageService : IWorkspaceService { ITemporaryStreamStorage CreateTemporaryStreamStorage(CancellationToken cancellationToken = default); ITemporaryTextStorage CreateTemporaryTextStorage(CancellationToken cancellationToken = default); } +/// +/// API to allow a client to write data to memory-mapped-file storage (allowing it to be shared across processes). +/// internal interface ITemporaryStorageServiceInternal : IWorkspaceService { - ITemporaryStreamStorageInternal CreateTemporaryStreamStorage(); - ITemporaryTextStorageInternal CreateTemporaryTextStorage(); + /// + /// Write the provided to a new memory-mapped-file. Returns a handle to the data that can + /// be used to identify the data across processes allowing it to be read back in in any process. + /// + /// + /// This type is primarily used to allow dumping metadata to disk. This then allowing them to be read in by mapping + /// their data into types like . It also allows them to be read in by our server + /// process, without having to transmit the data over the wire. + /// Note: The stream provided must support . The stream will also be reset to + /// 0 within this method. The caller does not need to reset the stream + /// itself. + /// + ITemporaryStorageStreamHandle WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken); + + /// + /// Write the provided to a new memory-mapped-file. Returns a handle to the data that can + /// be used to identify the data across processes allowing it to be read back in in any process. + /// + /// + /// This type is primarily used to allow dumping source texts to disk. This then allowing them to be read in by + /// mapping their data into types like . It also allows them + /// to be read in by our server process, without having to transmit the data over the wire. + /// + ITemporaryStorageTextHandle WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken); + + /// "/> + Task WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageStreamHandle.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageStreamHandle.cs new file mode 100644 index 0000000000000..418a8a7fe50cf --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageStreamHandle.cs @@ -0,0 +1,26 @@ +// 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.IO; +using System.Threading; + +namespace Microsoft.CodeAnalysis.Host; + +/// +/// Represents a handle to data stored to temporary storage (generally a memory mapped file). As long as this handle is +/// alive, the data should remain in storage and can be readable from any process using the information provided in . Use to write the data to temporary storage and get a handle to it. Use to read the data back in any process. +/// +internal interface ITemporaryStorageStreamHandle +{ + public TemporaryStorageIdentifier Identifier { get; } + + /// + /// Reads the data indicated to by this handle into a stream. This stream can be created in a different process + /// than the one that wrote the data originally. + /// + Stream ReadFromTemporaryStorage(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageTextHandle.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageTextHandle.cs new file mode 100644 index 0000000000000..d2c1dcccc88b1 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageTextHandle.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.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis.Host; + +internal interface ITemporaryStorageTextHandle +{ + public TemporaryStorageIdentifier Identifier { get; } + + SourceText ReadFromTemporaryStorage(CancellationToken cancellationToken); + Task ReadFromTemporaryStorageAsync(CancellationToken cancellationToken); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageWithName.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageWithName.cs deleted file mode 100644 index 3d635adbe6c8d..0000000000000 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStorageWithName.cs +++ /dev/null @@ -1,29 +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. - -namespace Microsoft.CodeAnalysis.Host; - -/// -/// TemporaryStorage can be used to read and write text to a temporary storage location. -/// -internal interface ITemporaryStorageWithName -{ - // TODO: clean up https://github.com/dotnet/roslyn/issues/43037 - // Name shouldn't be nullable. - - /// - /// Get name of the temporary storage - /// - string? Name { get; } - - /// - /// Get offset of the temporary storage - /// - long Offset { get; } - - /// - /// Get size of the temporary storage - /// - long Size { get; } -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStreamStorageExtensions.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStreamStorageExtensions.cs deleted file mode 100644 index af0d841bea463..0000000000000 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/ITemporaryStreamStorageExtensions.cs +++ /dev/null @@ -1,47 +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 System.IO; - -namespace Microsoft.CodeAnalysis.Host; - -internal static class ITemporaryStreamStorageExtensions -{ - public static void WriteAllLines(this ITemporaryStreamStorageInternal storage, ImmutableArray values) - { - using var stream = SerializableBytes.CreateWritableStream(); - using var writer = new StreamWriter(stream); - - foreach (var value in values) - { - writer.WriteLine(value); - } - - writer.Flush(); - stream.Position = 0; - - storage.WriteStream(stream); - } - - public static ImmutableArray ReadLines(this ITemporaryStreamStorageInternal storage) - { - return EnumerateLines(storage).ToImmutableArray(); - } - - private static IEnumerable EnumerateLines(ITemporaryStreamStorageInternal storage) - { - using var stream = storage.ReadStream(); - using var reader = new StreamReader(stream); - - string line; - while ((line = reader.ReadLine()) != null) - { - yield return line; - } - } -} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TemporaryStorageIdentifier.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TemporaryStorageIdentifier.cs new file mode 100644 index 0000000000000..2457297fca662 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TemporaryStorageIdentifier.cs @@ -0,0 +1,29 @@ +// 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.Runtime.Serialization; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.Host; + +/// +/// Identifier for a stream of data placed in a segment of temporary storage (generally a memory mapped file). Can be +/// used to identify that segment across processes, allowing for efficient sharing of data. +/// +internal sealed record TemporaryStorageIdentifier( + string Name, long Offset, long Size) +{ + public static TemporaryStorageIdentifier ReadFrom(ObjectReader reader) + => new( + reader.ReadRequiredString(), + reader.ReadInt64(), + reader.ReadInt64()); + + public void WriteTo(ObjectWriter writer) + { + writer.WriteString(Name); + writer.WriteInt64(Offset); + writer.WriteInt64(Size); + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.TrivialStorageStreamHandle.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.TrivialStorageStreamHandle.cs new file mode 100644 index 0000000000000..d3cb02f201d39 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.TrivialStorageStreamHandle.cs @@ -0,0 +1,26 @@ +// 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.IO; +using System.Threading; +using Microsoft.CodeAnalysis.Host; + +namespace Microsoft.CodeAnalysis; + +internal sealed partial class TrivialTemporaryStorageService +{ + private sealed class TrivialStorageStreamHandle( + TemporaryStorageIdentifier storageIdentifier, + MemoryStream streamCopy) : ITemporaryStorageStreamHandle + { + public TemporaryStorageIdentifier Identifier => storageIdentifier; + + public Stream ReadFromTemporaryStorage(CancellationToken cancellationToken) + { + // Return a read-only view of the underlying buffer to prevent users from overwriting or directly + // disposing the backing storage. + return new MemoryStream(streamCopy.GetBuffer(), 0, (int)streamCopy.Length, writable: false); + } + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.TrivialStorageTextHandle.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.TrivialStorageTextHandle.cs new file mode 100644 index 0000000000000..fb09daa83f991 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.TrivialStorageTextHandle.cs @@ -0,0 +1,26 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Host; +using Microsoft.CodeAnalysis.Text; + +namespace Microsoft.CodeAnalysis; + +internal sealed partial class TrivialTemporaryStorageService +{ + private sealed class TrivialStorageTextHandle( + TemporaryStorageIdentifier identifier, + SourceText sourceText) : ITemporaryStorageTextHandle + { + public TemporaryStorageIdentifier Identifier => identifier; + + public SourceText ReadFromTemporaryStorage(CancellationToken cancellationToken) + => sourceText; + + public Task ReadFromTemporaryStorageAsync(CancellationToken cancellationToken) + => Task.FromResult(ReadFromTemporaryStorage(cancellationToken)); + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs index 0f3641b459ca7..a9304b70e07a6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs +++ b/src/Workspaces/Core/Portable/Workspace/Host/TemporaryStorage/TrivialTemporaryStorageService.cs @@ -4,7 +4,6 @@ using System; using System.IO; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -12,7 +11,7 @@ namespace Microsoft.CodeAnalysis; -internal sealed class TrivialTemporaryStorageService : ITemporaryStorageServiceInternal +internal sealed partial class TrivialTemporaryStorageService : ITemporaryStorageServiceInternal { public static readonly TrivialTemporaryStorageService Instance = new(); @@ -20,92 +19,24 @@ private TrivialTemporaryStorageService() { } - public ITemporaryStreamStorageInternal CreateTemporaryStreamStorage() - => new StreamStorage(); - - public ITemporaryTextStorageInternal CreateTemporaryTextStorage() - => new TextStorage(); - - private sealed class StreamStorage : ITemporaryStreamStorageInternal + public ITemporaryStorageTextHandle WriteToTemporaryStorage(SourceText text, CancellationToken cancellationToken) { - private MemoryStream? _stream; - - public void Dispose() - { - _stream?.Dispose(); - _stream = null; - } - - public Stream ReadStream(CancellationToken cancellationToken) - { - var stream = _stream ?? throw new InvalidOperationException(); - - // Return a read-only view of the underlying buffer to prevent users from overwriting or directly - // disposing the backing storage. - return new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length, writable: false); - } - - public Task ReadStreamAsync(CancellationToken cancellationToken) - { - return Task.FromResult(ReadStream(cancellationToken)); - } - - public void WriteStream(Stream stream, CancellationToken cancellationToken) - { - var newStream = new MemoryStream(); - stream.CopyTo(newStream); - var existingValue = Interlocked.CompareExchange(ref _stream, newStream, null); - if (existingValue is not null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - } - - public async Task WriteStreamAsync(Stream stream, CancellationToken cancellationToken) - { - var newStream = new MemoryStream(); -#if NETCOREAPP - await stream.CopyToAsync(newStream, cancellationToken).ConfigureAwait(false); -# else - await stream.CopyToAsync(newStream).ConfigureAwait(false); -#endif - var existingValue = Interlocked.CompareExchange(ref _stream, newStream, null); - if (existingValue is not null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - } + var identifier = new TemporaryStorageIdentifier(Guid.NewGuid().ToString("N"), Offset: 0, Size: text.Length); + var handle = new TrivialStorageTextHandle(identifier, text); + return handle; } - private sealed class TextStorage : ITemporaryTextStorageInternal - { - private SourceText? _sourceText; - - public void Dispose() - => _sourceText = null; + public Task WriteToTemporaryStorageAsync(SourceText text, CancellationToken cancellationToken) + => Task.FromResult(WriteToTemporaryStorage(text, cancellationToken)); - public SourceText ReadText(CancellationToken cancellationToken) - => _sourceText ?? throw new InvalidOperationException(); - - public Task ReadTextAsync(CancellationToken cancellationToken) - => Task.FromResult(ReadText(cancellationToken)); - - public void WriteText(SourceText text, CancellationToken cancellationToken) - { - // This is a trivial implementation, indeed. Note, however, that we retain a strong - // reference to the source text, which defeats the intent of RecoverableTextAndVersion, but - // is appropriate for this trivial implementation. - var existingValue = Interlocked.CompareExchange(ref _sourceText, text, null); - if (existingValue is not null) - { - throw new InvalidOperationException(WorkspacesResources.Temporary_storage_cannot_be_written_more_than_once); - } - } + public ITemporaryStorageStreamHandle WriteToTemporaryStorage(Stream stream, CancellationToken cancellationToken) + { + var newStream = new MemoryStream(); + stream.CopyTo(newStream); + newStream.Position = 0; - public Task WriteTextAsync(SourceText text, CancellationToken cancellationToken = default) - { - WriteText(text, cancellationToken); - return Task.CompletedTask; - } + var identifier = new TemporaryStorageIdentifier(Guid.NewGuid().ToString("N"), Offset: 0, Size: stream.Length); + var handle = new TrivialStorageStreamHandle(identifier, newStream); + return handle; } } diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs index 04ba30d41afd4..0f6ed5627c13a 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.BatchingDocumentCollection.cs @@ -557,7 +557,7 @@ internal void UpdateSolutionForBatch( ClearAndZeroCapacity(_documentsAddedInBatch); // Document removing... - solutionChanges.UpdateSolutionForRemovedDocumentAction(removeDocuments(solutionChanges.Solution, _documentsRemovedInBatch.ToImmutableArray()), + solutionChanges.UpdateSolutionForRemovedDocumentAction(removeDocuments(solutionChanges.Solution, [.. _documentsRemovedInBatch]), removeDocumentChangeKind, _documentsRemovedInBatch); diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs index 6249e92df4226..4f387f594bed4 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProject.cs @@ -191,21 +191,30 @@ internal ProjectSystemProject( _filePath = filePath; _parseOptions = parseOptions; - var fileExtensionToWatch = language switch { LanguageNames.CSharp => ".cs", LanguageNames.VisualBasic => ".vb", _ => null }; + var watchedDirectories = GetWatchedDirectories(language, filePath); + _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(watchedDirectories); + _documentFileChangeContext.FileChanged += DocumentFileChangeContext_FileChanged; - if (filePath != null && fileExtensionToWatch != null) - { - // Since we have a project directory, we'll just watch all the files under that path; that'll avoid extra overhead of - // having to add explicit file watches everywhere. - var projectDirectoryToWatch = new WatchedDirectory(Path.GetDirectoryName(filePath)!, fileExtensionToWatch); - _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(projectDirectoryToWatch); - } - else + static WatchedDirectory[] GetWatchedDirectories(string? language, string? filePath) { - _documentFileChangeContext = _projectSystemProjectFactory.FileChangeWatcher.CreateContext(); - } + if (filePath is null) + { + return []; + } - _documentFileChangeContext.FileChanged += DocumentFileChangeContext_FileChanged; + var rootPath = Path.GetDirectoryName(filePath); + if (rootPath is null) + { + return []; + } + + return language switch + { + LanguageNames.VisualBasic => [new(rootPath, ".vb")], + LanguageNames.CSharp => [new(rootPath, ".cs"), new(rootPath, ".razor"), new(rootPath, ".cshtml")], + _ => [] + }; + } } private void ChangeProjectProperty(ref T field, T newValue, Func updateSolution, bool logThrowAwayTelemetry = false) diff --git a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs index 11be0ef802128..9c2059d1006c3 100644 --- a/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs +++ b/src/Workspaces/Core/Portable/Workspace/ProjectSystem/ProjectSystemProjectOptionsProcessor.cs @@ -3,8 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.Threading; using Microsoft.CodeAnalysis.Host; using Roslyn.Utilities; @@ -36,7 +38,7 @@ internal class ProjectSystemProjectOptionsProcessor : IDisposable /// (especially in cases with many references). /// /// Note: this will be null in the case that the command line is an empty array. - private ITemporaryStreamStorageInternal? _commandLineStorage; + private ITemporaryStorageStreamHandle? _commandLineStorageHandle; private CommandLineArguments _commandLineArgumentsForCommandLine; private string? _explicitRuleSetFilePath; @@ -71,12 +73,17 @@ private bool ReparseCommandLineIfChanged_NoLock(ImmutableArray arguments // Dispose the existing stored command-line and then persist the new one so we can // recover it later. Only bother persisting things if we have a non-empty string. - _commandLineStorage?.Dispose(); - _commandLineStorage = null; + _commandLineStorageHandle = null; if (!arguments.IsEmpty) { - _commandLineStorage = _temporaryStorageService.CreateTemporaryStreamStorage(); - _commandLineStorage.WriteAllLines(arguments); + using var stream = SerializableBytes.CreateWritableStream(); + using var writer = new StreamWriter(stream); + + foreach (var value in arguments) + writer.WriteLine(value); + + writer.Flush(); + _commandLineStorageHandle = _temporaryStorageService.WriteToTemporaryStorage(stream, CancellationToken.None); } ReparseCommandLine_NoLock(arguments); @@ -237,12 +244,24 @@ private void RuleSetFile_UpdatedOnDisk(object? sender, EventArgs e) // effective values was potentially done by the act of parsing the command line. Even though the command line didn't change textually, // the effective result did. Then we call UpdateProjectOptions_NoLock to reapply any values; that will also re-acquire the new ruleset // includes in the IDE so we can be watching for changes again. - var commandLine = _commandLineStorage == null ? ImmutableArray.Empty : _commandLineStorage.ReadLines(); + var commandLine = _commandLineStorageHandle == null + ? ImmutableArray.Empty + : EnumerateLines(_commandLineStorageHandle).ToImmutableArray(); DisposeOfRuleSetFile_NoLock(); ReparseCommandLine_NoLock(commandLine); UpdateProjectOptions_NoLock(); } + + static IEnumerable EnumerateLines( + ITemporaryStorageStreamHandle storageHandle) + { + using var stream = storageHandle.ReadFromTemporaryStorage(CancellationToken.None); + using var reader = new StreamReader(stream); + + while (reader.ReadLine() is string line) + yield return line; + } } /// @@ -276,7 +295,6 @@ public void Dispose() lock (_gate) { DisposeOfRuleSetFile_NoLock(); - _commandLineStorage?.Dispose(); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs index 95519835568d7..ead02e2f244d2 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Checksum_Factory.cs @@ -105,16 +105,12 @@ public static Checksum Create(ImmutableArray bytes) writer.WriteByte(b); }); - public static Checksum Create(T value, ISerializerService serializer) - { - using var context = new SolutionReplicationContext(); - - return Create( - (value, serializer, context), + public static Checksum Create(T value, ISerializerService serializer, CancellationToken cancellationToken) + => Create( + (value, serializer, cancellationToken), static (tuple, writer) => { - var (value, serializer, context) = tuple; - serializer.Serialize(value!, writer, context, CancellationToken.None); + var (value, serializer, cancellationToken) = tuple; + serializer.Serialize(value!, writer, cancellationToken); }); - } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs index 32714a34a1c44..3313ec6f1c845 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ConstantTextAndVersionSource.cs @@ -19,6 +19,12 @@ internal sealed class ConstantTextAndVersionSource(TextAndVersion value) : IText public bool CanReloadText => false; + /// + /// Not built from a text loader. + /// + public TextLoader? TextLoader + => null; + public TextAndVersion GetValue(LoadTextOptions options, CancellationToken cancellationToken) => _value; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index 7f2bffd0dd681..379fabf46b317 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -326,60 +326,56 @@ internal async ValueTask GetRequiredNullableDisabledSemanticModel /// private async Task GetSemanticModelHelperAsync(bool disableNullableAnalysis, CancellationToken cancellationToken) { - try - { - if (!this.SupportsSemanticModel) - { - return null; - } + if (!this.SupportsSemanticModel) + return null; - SemanticModel? semanticModel; - if (disableNullableAnalysis) + var semanticModel = await GetSemanticModelWorkerAsync().ConfigureAwait(false); + this.Project.Solution.OnSemanticModelObtained(this.Id, semanticModel); + return semanticModel; + + async Task GetSemanticModelWorkerAsync() + { + try { - if (this.TryGetNullableDisabledSemanticModel(out semanticModel)) + if (disableNullableAnalysis) { - return semanticModel; + if (this.TryGetNullableDisabledSemanticModel(out var semanticModel)) + return semanticModel; } - } - else - { - if (this.TryGetSemanticModel(out semanticModel)) + else { - return semanticModel; + if (this.TryGetSemanticModel(out var semanticModel)) + return semanticModel; } - } - var syntaxTree = await this.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); - var compilation = await this.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); + var syntaxTree = await this.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); + var compilation = await this.Project.GetRequiredCompilationAsync(cancellationToken).ConfigureAwait(false); #pragma warning disable RSEXPERIMENTAL001 // sym-shipped usage of experimental API - var result = compilation.GetSemanticModel(syntaxTree, disableNullableAnalysis ? SemanticModelOptions.DisableNullableAnalysis : SemanticModelOptions.None); + var result = compilation.GetSemanticModel(syntaxTree, disableNullableAnalysis ? SemanticModelOptions.DisableNullableAnalysis : SemanticModelOptions.None); #pragma warning restore RSEXPERIMENTAL001 - Contract.ThrowIfNull(result); - var original = Interlocked.CompareExchange(ref disableNullableAnalysis ? ref _nullableDisabledModel : ref _model, new WeakReference(result), null); + Contract.ThrowIfNull(result); + var original = Interlocked.CompareExchange(ref disableNullableAnalysis ? ref _nullableDisabledModel : ref _model, new WeakReference(result), null); - // okay, it is first time. - if (original == null) - { - return result; - } + // okay, it is first time. + if (original == null) + return result; - // It looks like someone has set it. Try to reuse same semantic model, or assign the new model if that - // fails. The lock is required since there is no compare-and-set primitive for WeakReference. - lock (original) - { - if (original.TryGetTarget(out semanticModel)) + // It looks like someone has set it. Try to reuse same semantic model, or assign the new model if that + // fails. The lock is required since there is no compare-and-set primitive for WeakReference. + lock (original) { - return semanticModel; - } + if (original.TryGetTarget(out var semanticModel)) + return semanticModel; - original.SetTarget(result); - return result; + original.SetTarget(result); + return result; + } + } + catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) + { + throw ExceptionUtilities.Unreachable(); } - } - catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) - { - throw ExceptionUtilities.Unreachable(); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs index 9c92c71478ee4..8e7daaa323d16 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState.cs @@ -31,7 +31,7 @@ internal partial class DocumentState : TextDocumentState private readonly ParseOptions? _options; // null if the document doesn't support syntax trees: - private readonly AsyncLazy? _treeSource; + private readonly ITreeAndVersionSource? _treeSource; protected DocumentState( LanguageServices languageServices, @@ -40,7 +40,7 @@ protected DocumentState( ParseOptions? options, ITextAndVersionSource textSource, LoadTextOptions loadTextOptions, - AsyncLazy? treeSource) + ITreeAndVersionSource? treeSource) : base(languageServices.SolutionServices, documentServiceProvider, attributes, textSource, loadTextOptions) { Contract.ThrowIfFalse(_options is null == _treeSource is null); @@ -79,7 +79,7 @@ public DocumentState( } } - public AsyncLazy? TreeSource => _treeSource; + public ITreeAndVersionSource? TreeSource => _treeSource; [MemberNotNullWhen(true, nameof(_treeSource))] [MemberNotNullWhen(true, nameof(TreeSource))] @@ -97,7 +97,7 @@ public SourceCodeKind SourceCodeKind public bool IsGenerated => Attributes.IsGenerated; - protected static AsyncLazy CreateLazyFullyParsedTree( + protected static ITreeAndVersionSource CreateLazyFullyParsedTree( ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, string? filePath, @@ -105,7 +105,7 @@ protected static AsyncLazy CreateLazyFullyParsedTree( LanguageServices languageServices, PreservationMode mode = PreservationMode.PreserveValue) { - return AsyncLazy.Create( + return SimpleTreeAndVersionSource.Create( static (arg, c) => FullyParseTreeAsync(arg.newTextSource, arg.loadTextOptions, arg.filePath, arg.options, arg.languageServices, arg.mode, c), static (arg, c) => FullyParseTree(arg.newTextSource, arg.loadTextOptions, arg.filePath, arg.options, arg.languageServices, arg.mode, c), arg: (newTextSource, loadTextOptions, filePath, options, languageServices, mode)); @@ -163,19 +163,19 @@ private static TreeAndVersion CreateTreeAndVersion( return new TreeAndVersion(tree, textAndVersion.Version); } - private static AsyncLazy CreateLazyIncrementallyParsedTree( - AsyncLazy oldTreeSource, + private static ITreeAndVersionSource CreateLazyIncrementallyParsedTree( + ITreeAndVersionSource oldTreeSource, ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions) { - return AsyncLazy.Create( + return SimpleTreeAndVersionSource.Create( static (arg, c) => IncrementallyParseTreeAsync(arg.oldTreeSource, arg.newTextSource, arg.loadTextOptions, c), static (arg, c) => IncrementallyParseTree(arg.oldTreeSource, arg.newTextSource, arg.loadTextOptions, c), arg: (oldTreeSource, newTextSource, loadTextOptions)); } private static async Task IncrementallyParseTreeAsync( - AsyncLazy oldTreeSource, + ITreeAndVersionSource oldTreeSource, ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, CancellationToken cancellationToken) @@ -197,7 +197,7 @@ private static async Task IncrementallyParseTreeAsync( } private static TreeAndVersion IncrementallyParseTree( - AsyncLazy oldTreeSource, + ITreeAndVersionSource oldTreeSource, ITextAndVersionSource newTextSource, LoadTextOptions loadTextOptions, CancellationToken cancellationToken) @@ -346,7 +346,7 @@ private DocumentState SetParseOptions(ParseOptions options, bool onlyPreprocesso throw new InvalidOperationException(); } - AsyncLazy? newTreeSource = null; + ITreeAndVersionSource? newTreeSource = null; // Optimization: if we are only changing preprocessor directives, and we've already parsed the existing tree // and it didn't have any, we can avoid a reparse since the tree will be parsed the same. @@ -368,7 +368,7 @@ private DocumentState SetParseOptions(ParseOptions options, bool onlyPreprocesso } if (newTree is not null) - newTreeSource = AsyncLazy.Create(new TreeAndVersion(newTree, existingTreeAndVersion.Version)); + newTreeSource = SimpleTreeAndVersionSource.Create(new TreeAndVersion(newTree, existingTreeAndVersion.Version)); } // If we weren't able to reuse in a smart way, just reparse @@ -457,7 +457,7 @@ public DocumentState UpdateFilePath(string? filePath) protected override TextDocumentState UpdateText(ITextAndVersionSource newTextSource, PreservationMode mode, bool incremental) { - AsyncLazy? newTreeSource; + ITreeAndVersionSource? newTreeSource; if (!SupportsSyntaxTree) { @@ -528,7 +528,7 @@ internal DocumentState UpdateTree(SyntaxNode newRoot, PreservationMode mode) _options, textSource: text, LoadTextOptions, - treeSource: AsyncLazy.Create(treeAndVersion)); + treeSource: SimpleTreeAndVersionSource.Create(treeAndVersion)); // use static method so we don't capture references to this static (ITextAndVersionSource, TreeAndVersion) CreateTreeWithLazyText( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs index 9353d1f140a87..8c481c1b4a3be 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_LinkedFileReuse.cs @@ -13,6 +13,29 @@ namespace Microsoft.CodeAnalysis; internal partial class DocumentState { + /// + /// when we're linked to another file (a 'sibling') and will attempt to reuse + /// that sibling's tree as our own. Note: we won't know if we can actually use the contents of that sibling file + /// until we actually go and realize it, as it may contains constructs (like pp-directives) that prevent use. In + /// that case, we'll fall back to a normal incremental parse between our original and the latest text contents of our sibling's file. + /// + private sealed class LinkedFileReuseTreeAndVersionSource( + ITreeAndVersionSource originalTreeSource, + AsyncLazy lazyComputation) : ITreeAndVersionSource + { + public readonly ITreeAndVersionSource OriginalTreeSource = originalTreeSource; + + public Task GetValueAsync(CancellationToken cancellationToken) + => lazyComputation.GetValueAsync(cancellationToken); + + public TreeAndVersion GetValue(CancellationToken cancellationToken) + => lazyComputation.GetValue(cancellationToken); + + public bool TryGetValue([NotNullWhen(true)] out TreeAndVersion? value) + => lazyComputation.TryGetValue(out value); + } + /// /// Returns a new instance of this document state that points to as the /// text contents of the document, and which will produce a syntax tree that reuses from public DocumentState UpdateTextAndTreeContents( ITextAndVersionSource siblingTextSource, - AsyncLazy? siblingTreeSource, + ITreeAndVersionSource? siblingTreeSource, bool forceEvenIfTreesWouldDiffer) { if (!SupportsSyntaxTree) @@ -38,50 +61,52 @@ public DocumentState UpdateTextAndTreeContents( Contract.ThrowIfNull(siblingTreeSource); + // We don't want to point at a long chain of transformations as our sibling files change, deferring to each next + // link of the chain to potentially do the work (or potentially failing out). So, if we're about to do this, + // instead return our original tree-source so that in the case we are unable to use the sibling file's root, we + // can do a single step incremental parse between our original tree and the final sibling text. + // + // We only need to look one deep here as we'll pull that tree source forward to our level. If another link is + // later added to us, it will do the same thing. + var originalTreeSource = this.TreeSource; + if (originalTreeSource is LinkedFileReuseTreeAndVersionSource linkedFileTreeAndVersionSource) + originalTreeSource = linkedFileTreeAndVersionSource.OriginalTreeSource; + // Always pass along the sibling text. We will always be in sync with that. - // if a sibling tree source is provided, then we'll want to attempt to use the tree it creates, to share as - // much memory as possible with linked files. However, we can't point at that source directly. If we did, - // we'd produce the *exact* same tree-reference as another file. That would be bad as it would break the - // invariant that each document gets a unique SyntaxTree. So, instead, we produce a ValueSource that defers - // to the provided source, gets the tree from it, and then wraps its root in a new tree for us. + // Defer to static helper to make sure we don't accidentally capture anything else we don't want off of 'this' + // (like "this.TreeSource"). + return UpdateTextAndTreeContentsWorker( + this.Attributes, this.LanguageServices, this.Services, this.LoadTextOptions, this.ParseOptions, + originalTreeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer); + } - // copy data from this entity, and pass to static helper, so we don't keep this green node alive. + private static DocumentState UpdateTextAndTreeContentsWorker( + DocumentInfo.DocumentAttributes attributes, + LanguageServices languageServices, + IDocumentServiceProvider services, + LoadTextOptions loadTextOptions, + ParseOptions parseOptions, + ITreeAndVersionSource originalTreeSource, + ITextAndVersionSource siblingTextSource, + ITreeAndVersionSource siblingTreeSource, + bool forceEvenIfTreesWouldDiffer) + { + // if a sibling tree source is provided, then we'll want to attempt to use the tree it creates, to share as much + // memory as possible with linked files. However, we can't point at that source directly. If we did, we'd + // produce the *exact* same tree-reference as another file. That would be bad as it would break the invariant + // that each document gets a unique SyntaxTree. So, instead, we produce a tree-source that defers to the + // provided source, gets the tree from it, and then wraps its root in a new tree for us. - var filePath = this.Attributes.SyntaxTreeFilePath; - var languageServices = this.LanguageServices; - var loadTextOptions = this.LoadTextOptions; - var parseOptions = this.ParseOptions; - var textAndVersionSource = this.TextAndVersionSource; - var treeSource = this.TreeSource; + var lazyComputation = AsyncLazy.Create( + static (arg, cancellationToken) => TryReuseSiblingTreeAsync(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.originalTreeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), + static (arg, cancellationToken) => TryReuseSiblingTree(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.originalTreeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), + arg: (filePath: attributes.SyntaxTreeFilePath, languageServices, loadTextOptions, parseOptions, originalTreeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer)); - var newTreeSource = GetReuseTreeSource( - filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer); + var newTreeSource = new LinkedFileReuseTreeAndVersionSource(originalTreeSource, lazyComputation); return new DocumentState( - languageServices, - Services, - Attributes, - _options, - siblingTextSource, - LoadTextOptions, - newTreeSource); - - static AsyncLazy GetReuseTreeSource( - string filePath, - LanguageServices languageServices, - LoadTextOptions loadTextOptions, - ParseOptions parseOptions, - AsyncLazy treeSource, - ITextAndVersionSource siblingTextSource, - AsyncLazy siblingTreeSource, - bool forceEvenIfTreesWouldDiffer) - { - return AsyncLazy.Create( - static (arg, cancellationToken) => TryReuseSiblingTreeAsync(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.treeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), - static (arg, cancellationToken) => TryReuseSiblingTree(arg.filePath, arg.languageServices, arg.loadTextOptions, arg.parseOptions, arg.treeSource, arg.siblingTextSource, arg.siblingTreeSource, arg.forceEvenIfTreesWouldDiffer, cancellationToken), - arg: (filePath, languageServices, loadTextOptions, parseOptions, treeSource, siblingTextSource, siblingTreeSource, forceEvenIfTreesWouldDiffer)); - } + languageServices, services, attributes, parseOptions, siblingTextSource, loadTextOptions, newTreeSource); static bool TryReuseSiblingRoot( string filePath, @@ -186,9 +211,9 @@ static async Task TryReuseSiblingTreeAsync( LanguageServices languageServices, LoadTextOptions loadTextOptions, ParseOptions parseOptions, - AsyncLazy treeSource, + ITreeAndVersionSource treeSource, ITextAndVersionSource siblingTextSource, - AsyncLazy siblingTreeSource, + ITreeAndVersionSource siblingTreeSource, bool forceEvenIfTreesWouldDiffer, CancellationToken cancellationToken) { @@ -209,9 +234,9 @@ static TreeAndVersion TryReuseSiblingTree( LanguageServices languageServices, LoadTextOptions loadTextOptions, ParseOptions parseOptions, - AsyncLazy treeSource, + ITreeAndVersionSource treeSource, ITextAndVersionSource siblingTextSource, - AsyncLazy siblingTreeSource, + ITreeAndVersionSource siblingTreeSource, bool forceEvenIfTreesWouldDiffer, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs index 30eaca390f9cb..42553e056311c 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/DocumentState_TreeTextSource.cs @@ -22,6 +22,12 @@ private sealed class TreeTextSource(AsyncLazy textSource, VersionSta public bool CanReloadText => false; + /// + /// Not created from a text loader. + /// + public TextLoader? TextLoader + => null; + public async Task GetValueAsync(LoadTextOptions options, CancellationToken cancellationToken) { var text = await textSource.GetValueAsync(cancellationToken).ConfigureAwait(false); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs index 69e3298087b6b..db5c420b28c49 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph.cs @@ -270,7 +270,7 @@ private ImmutableHashSet GetProjectsThatThisProjectTransitivelyDepend using var pooledObject = SharedPools.Default>().GetPooledObject(); var results = pooledObject.Object; this.ComputeTransitiveReferences(projectId, results); - transitiveReferences = results.ToImmutableHashSet(); + transitiveReferences = [.. results]; _transitiveReferencesMap = _transitiveReferencesMap.Add(projectId, transitiveReferences); } @@ -323,7 +323,7 @@ private ImmutableHashSet GetProjectsThatTransitivelyDependOnThisProje var results = pooledObject.Object; ComputeReverseTransitiveReferences(projectId, results); - reverseTransitiveReferences = results.ToImmutableHashSet(); + reverseTransitiveReferences = [.. results]; _reverseTransitiveReferencesMap = _reverseTransitiveReferencesMap.Add(projectId, reverseTransitiveReferences); } @@ -367,7 +367,7 @@ private void GetTopologicallySortedProjects_NoLock(CancellationToken cancellatio using var seenProjects = SharedPools.Default>().GetPooledObject(); using var resultList = SharedPools.Default>().GetPooledObject(); this.TopologicalSort(_projectIds, seenProjects.Object, resultList.Object, cancellationToken); - _lazyTopologicallySortedProjects = resultList.Object.ToImmutableArray(); + _lazyTopologicallySortedProjects = [.. resultList.Object]; } } @@ -419,7 +419,7 @@ private ImmutableArray> GetDependencySets_NoLock(Cancella using var seenProjects = SharedPools.Default>().GetPooledObject(); using var results = SharedPools.Default>>().GetPooledObject(); this.ComputeDependencySets(seenProjects.Object, results.Object, cancellationToken); - _lazyDependencySets = results.Object.ToImmutableArray(); + _lazyDependencySets = [.. results.Object]; } return _lazyDependencySets; diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs index 56a51c3f5750a..d7e48d3bf3dd3 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/ProjectDependencyGraph_AddProjectReference.cs @@ -66,7 +66,7 @@ private static ImmutableDictionary> Compu } else { - return existingReferencesMap.SetItem(projectId, referencedProjectIds.ToImmutableHashSet()); + return existingReferencesMap.SetItem(projectId, [.. referencedProjectIds]); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs index e0a5cd666f168..11b1bd8884d08 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.CompilationTracker.cs @@ -529,18 +529,23 @@ await compilationState.GetCompilationAsync( if (creationPolicy.SkeletonReferenceCreationPolicy is SkeletonReferenceCreationPolicy.Create) { - // Client always wants an up to date metadata reference. Produce one for this project reference. - var metadataReference = await compilationState.GetMetadataReferenceAsync(projectReference, this.ProjectState, cancellationToken).ConfigureAwait(false); + // Client always wants an up to date metadata reference. Produce one for this project + // reference. Because the policy is to always 'Create' here, we include cross language + // references, producing skeletons for them if necessary. + var metadataReference = await compilationState.GetMetadataReferenceAsync( + projectReference, this.ProjectState, includeCrossLanguage: true, cancellationToken).ConfigureAwait(false); AddMetadataReference(projectReference, metadataReference); } else { Contract.ThrowIfFalse(creationPolicy.SkeletonReferenceCreationPolicy is SkeletonReferenceCreationPolicy.CreateIfAbsent or SkeletonReferenceCreationPolicy.DoNotCreate); - // If not asked to explicit create an up to date skeleton, attempt to get a partial - // reference, or fallback to the last successful reference for this project if we can - // find one. - var metadataReference = compilationState.GetPartialMetadataReference(projectReference, this.ProjectState); + // Client does not want to force a skeleton reference to be created. Try to get a + // metadata reference cheaply in the case where this is a reference to the same + // language. If that fails, also attempt to get a reference to a skeleton assembly + // produced from one of our prior stale compilations. + var metadataReference = await compilationState.GetMetadataReferenceAsync( + projectReference, this.ProjectState, includeCrossLanguage: false, cancellationToken).ConfigureAwait(false); if (metadataReference is null) { var inProgressCompilationNotRef = staleCompilationWithGeneratedDocuments ?? compilationWithoutGeneratedDocuments; @@ -552,7 +557,8 @@ await compilationState.GetCompilationAsync( // create a real skeleton here. if (metadataReference is null && creationPolicy.SkeletonReferenceCreationPolicy is SkeletonReferenceCreationPolicy.CreateIfAbsent) { - metadataReference = await compilationState.GetMetadataReferenceAsync(projectReference, this.ProjectState, cancellationToken).ConfigureAwait(false); + metadataReference = await compilationState.GetMetadataReferenceAsync( + projectReference, this.ProjectState, includeCrossLanguage: true, cancellationToken).ConfigureAwait(false); } AddMetadataReference(projectReference, metadataReference); @@ -647,32 +653,6 @@ private Compilation CreateEmptyCompilation() } } - /// - /// Attempts to get (without waiting) a metadata reference to a possibly in progress - /// compilation. Only actual compilation references are returned. Could potentially - /// return null if nothing can be provided. - /// - public MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference) - { - if (ProjectState.LanguageServices == fromProject.LanguageServices) - { - // if we have a compilation and its the correct language, use a simple compilation reference in any - // state it happens to be in right now - if (ReadState() is CompilationTrackerState compilationState) - return compilationState.CompilationWithoutGeneratedDocuments.ToMetadataReference(projectReference.Aliases, projectReference.EmbedInteropTypes); - } - else - { - // Cross project reference. We need a skeleton reference. Skeletons are too expensive to - // generate on demand. So just try to see if we can grab the last generated skeleton for that - // project. - var properties = new MetadataReferenceProperties(aliases: projectReference.Aliases, embedInteropTypes: projectReference.EmbedInteropTypes); - return _skeletonReferenceCache.TryGetAlreadyBuiltMetadataReference(properties); - } - - return null; - } - public Task HasSuccessfullyLoadedAsync( SolutionCompilationState compilationState, CancellationToken cancellationToken) { @@ -1019,7 +999,7 @@ private static void ValidateCompilationTreesMatchesProjectState(Compilation comp } /// - /// This is just the same as but throws a custom exception type to make this easier to find in telemetry since the exception type + /// This is just the same as but throws a custom exception type to make this easier to find in telemetry since the exception type /// is easily seen in telemetry. /// private static void ThrowExceptionIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs index 990d4467db2a7..83dd3fa0a8ce6 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.GeneratedFileReplacingCompilationTracker.cs @@ -147,18 +147,6 @@ private async Task ComputeDependentChecksumAsync(SolutionCompilationSt await UnderlyingTracker.GetDependentChecksumAsync(compilationState, cancellationToken).ConfigureAwait(false), (await _replacementDocumentStates.GetDocumentChecksumsAndIdsAsync(cancellationToken).ConfigureAwait(false)).Checksum); - public MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference) - { - // This method is used if you're forking a solution with partial semantics, and used to quickly produce references. - // So this method should only be called if: - // - // 1. Project A has a open source generated document, and this CompilationTracker represents A - // 2. Project B references that A, and is being frozen for partial semantics. - // - // We generally don't use partial semantics in a different project than the open file, so this isn't a scenario we need to support. - throw new NotImplementedException(); - } - public async ValueTask> GetSourceGeneratedDocumentStatesAsync( SolutionCompilationState compilationState, CancellationToken cancellationToken) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs index c910b9301214d..30455ec7cfd49 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.ICompilationTracker.cs @@ -49,7 +49,6 @@ private interface ICompilationTracker Task GetDependentSemanticVersionAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); Task GetDependentChecksumAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); - MetadataReference? GetPartialMetadataReference(ProjectState fromProject, ProjectReference projectReference); ValueTask> GetSourceGeneratedDocumentStatesAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); ValueTask> GetSourceGeneratorDiagnosticsAsync(SolutionCompilationState compilationState, CancellationToken cancellationToken); ValueTask GetSourceGeneratorRunResultAsync(SolutionCompilationState solution, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs index f0e1ffc395db2..2fbf3bc719d90 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceCache.cs @@ -218,74 +218,79 @@ public readonly SkeletonReferenceCache Clone() private static SkeletonReferenceSet? CreateSkeletonSet( SolutionServices services, Compilation compilation, CancellationToken cancellationToken) { - var storage = TryCreateMetadataStorage(services, compilation, cancellationToken); - if (storage == null) + var (metadata, storageHandle) = TryCreateMetadataAndHandle(); + if (metadata == null) return null; - var metadata = AssemblyMetadata.CreateFromStream(storage.ReadStream(cancellationToken), leaveOpen: false); - // read in the stream and pass ownership of it to the metadata object. When it is disposed it will dispose // the stream as well. return new SkeletonReferenceSet( metadata, + storageHandle, compilation.AssemblyName, new DeferredDocumentationProvider(compilation)); - } - - private static ITemporaryStreamStorageInternal? TryCreateMetadataStorage(SolutionServices services, Compilation compilation, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - - var logger = services.GetService(); - try + (AssemblyMetadata? metadata, ITemporaryStorageStreamHandle storageHandle) TryCreateMetadataAndHandle() { - logger?.Log($"Beginning to create a skeleton assembly for {compilation.AssemblyName}..."); + cancellationToken.ThrowIfCancellationRequested(); - using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_EmitMetadataOnlyImage, cancellationToken)) - { - using var stream = SerializableBytes.CreateWritableStream(); + var logger = services.GetService(); - var emitResult = compilation.Emit(stream, options: s_metadataOnlyEmitOptions, cancellationToken: cancellationToken); + try + { + logger?.Log($"Beginning to create a skeleton assembly for {compilation.AssemblyName}..."); - if (emitResult.Success) + using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_EmitMetadataOnlyImage, cancellationToken)) { - logger?.Log($"Successfully emitted a skeleton assembly for {compilation.AssemblyName}"); + // First, emit the data to an in-memory stream. + using var stream = SerializableBytes.CreateWritableStream(); + var emitResult = compilation.Emit(stream, options: s_metadataOnlyEmitOptions, cancellationToken: cancellationToken); - var temporaryStorageService = services.GetRequiredService(); - var storage = temporaryStorageService.CreateTemporaryStreamStorage(); + if (emitResult.Success) + { + logger?.Log($"Successfully emitted a skeleton assembly for {compilation.AssemblyName}"); - stream.Position = 0; - storage.WriteStream(stream, cancellationToken); + var temporaryStorageService = services.GetRequiredService(); - return storage; - } + // Then, dump that in-memory-stream to a memory-mapped file. Doing this allows us to have the + // assembly-metadata point directly to that pointer in memory, instead of it having to make its + // own copy it needs to own the lifetime of. + var handle = temporaryStorageService.WriteToTemporaryStorage(stream, cancellationToken); - if (logger != null) - { - logger.Log($"Failed to create a skeleton assembly for {compilation.AssemblyName}:"); + // Now read the data back from the stream from the memory mapped file. This will come back as an + // UnmanagedMemoryStream, which our assembly/metadata subsystem is optimized around. + var result = AssemblyMetadata.CreateFromStream( + handle.ReadFromTemporaryStorage(cancellationToken), leaveOpen: false); - foreach (var diagnostic in emitResult.Diagnostics) + return (result, handle); + } + + if (logger != null) { - logger.Log(" " + diagnostic.GetMessage()); + logger.Log($"Failed to create a skeleton assembly for {compilation.AssemblyName}:"); + + foreach (var diagnostic in emitResult.Diagnostics) + { + logger.Log(" " + diagnostic.GetMessage()); + } } - } - // log emit failures so that we can improve most common cases - Logger.Log(FunctionId.MetadataOnlyImage_EmitFailure, KeyValueLogMessage.Create(m => - { - // log errors in the format of - // CS0001:1;CS002:10;... - var groups = emitResult.Diagnostics.GroupBy(d => d.Id).Select(g => $"{g.Key}:{g.Count()}"); - m["Errors"] = string.Join(";", groups); - })); + // log emit failures so that we can improve most common cases + Logger.Log(FunctionId.MetadataOnlyImage_EmitFailure, KeyValueLogMessage.Create(m => + { + // log errors in the format of + // CS0001:1;CS002:10;... + var groups = emitResult.Diagnostics.GroupBy(d => d.Id).Select(g => $"{g.Key}:{g.Count()}"); + m["Errors"] = string.Join(";", groups); + })); - return null; + return (null, null!); + } + } + finally + { + logger?.Log($"Done trying to create a skeleton assembly for {compilation.AssemblyName}"); } - } - finally - { - logger?.Log($"Done trying to create a skeleton assembly for {compilation.AssemblyName}"); } } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs index 033c900fecff5..e3e9b84b07744 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.SkeletonReferenceSet.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using Microsoft.CodeAnalysis.Host; namespace Microsoft.CodeAnalysis; @@ -18,6 +19,7 @@ internal partial class SolutionCompilationState /// private sealed class SkeletonReferenceSet( AssemblyMetadata metadata, + ITemporaryStorageStreamHandle storageHandle, string? assemblyName, DeferredDocumentationProvider documentationProvider) { @@ -29,6 +31,8 @@ private sealed class SkeletonReferenceSet( /// private readonly Dictionary _referenceMap = []; + public ITemporaryStorageStreamHandle StorageHandle => storageHandle; + public PortableExecutableReference GetOrCreateMetadataReference(MetadataReferenceProperties properties) { lock (_referenceMap) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs index 899adeef5bfe6..058055eb643fe 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionCompilationState.cs @@ -1009,24 +1009,6 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( return GetCompilationTracker(documentId.ProjectId).TryGetSourceGeneratedDocumentStateForAlreadyGeneratedId(documentId); } - /// - /// Attempt to get the best readily available compilation for the project. It may be a - /// partially built compilation. - /// - private MetadataReference? GetPartialMetadataReference( - ProjectReference projectReference, - ProjectState fromProject) - { - // Try to get the compilation state for this project. If it doesn't exist, don't do any - // more work. - if (!_projectIdToTrackerMap.TryGetValue(projectReference.ProjectId, out var state)) - { - return null; - } - - return state.GetPartialMetadataReference(fromProject, projectReference); - } - /// /// Get a metadata reference to this compilation info's compilation with respect to /// another project. For cross language references produce a skeletal assembly. If the @@ -1034,7 +1016,7 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( /// needed and does not exist, it is also built. /// private async Task GetMetadataReferenceAsync( - ICompilationTracker tracker, ProjectState fromProject, ProjectReference projectReference, CancellationToken cancellationToken) + ICompilationTracker tracker, ProjectState fromProject, ProjectReference projectReference, bool includeCrossLanguage, CancellationToken cancellationToken) { try { @@ -1046,6 +1028,9 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( return compilation.ToMetadataReference(projectReference.Aliases, projectReference.EmbedInteropTypes); } + if (!includeCrossLanguage) + return null; + // otherwise get a metadata only image reference that is built by emitting the metadata from the // referenced project's compilation and re-importing it. using (Logger.LogBlock(FunctionId.Workspace_SkeletonAssembly_GetMetadataOnlyImage, cancellationToken)) @@ -1065,14 +1050,14 @@ public ValueTask> GetSourceGeneratorDiagnosticsAsync( /// can happen when trying to build a skeleton reference that fails to build. /// public Task GetMetadataReferenceAsync( - ProjectReference projectReference, ProjectState fromProject, CancellationToken cancellationToken) + ProjectReference projectReference, ProjectState fromProject, bool includeCrossLanguage, CancellationToken cancellationToken) { try { // Get the compilation state for this project. If it's not already created, then this // will create it. Then force that state to completion and get a metadata reference to it. var tracker = this.GetCompilationTracker(projectReference.ProjectId); - return GetMetadataReferenceAsync(tracker, fromProject, projectReference, cancellationToken); + return GetMetadataReferenceAsync(tracker, fromProject, projectReference, includeCrossLanguage, cancellationToken); } catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical)) { @@ -1176,7 +1161,7 @@ public SolutionCompilationState WithFrozenSourceGeneratedDocuments( var documentStatesByProjectId = documentStates.ToDictionary(static state => state.Id.ProjectId); var newTrackerMap = CreateCompilationTrackerMap( - documentStatesByProjectId.Keys.ToImmutableArray(), + [.. documentStatesByProjectId.Keys], this.SolutionState.GetProjectDependencyGraph(), static (trackerMap, arg) => { @@ -1570,7 +1555,7 @@ private SolutionCompilationState RemoveDocumentsFromMultipleProjects( var removedDocumentStatesForProject = removedDocumentStates.ToImmutable(); - var compilationTranslationAction = removeDocumentsFromProjectState(oldProjectState, documentIdsInProject.ToImmutableArray(), removedDocumentStatesForProject); + var compilationTranslationAction = removeDocumentsFromProjectState(oldProjectState, [.. documentIdsInProject], removedDocumentStatesForProject); var newProjectState = compilationTranslationAction.NewProjectState; var stateChange = newCompilationState.SolutionState.ForkProject( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index c23854f2e7d35..9600215792ec1 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -1178,7 +1178,7 @@ public static ProjectDependencyGraph CreateDependencyGraph( state.ProjectReferences.Where(pr => projectStates.ContainsKey(pr.ProjectId)).Select(pr => pr.ProjectId).ToImmutableHashSet())) .ToImmutableDictionary(); - return new ProjectDependencyGraph(projectIds.ToImmutableHashSet(), map); + return new ProjectDependencyGraph([.. projectIds], map); } public SolutionState WithOptions(SolutionOptionSet options) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution_SemanticModelCaching.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution_SemanticModelCaching.cs new file mode 100644 index 0000000000000..32bebdc86d8d5 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution_SemanticModelCaching.cs @@ -0,0 +1,59 @@ +// 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; + +public partial class Solution +{ + /// + /// Strongly held reference to the semantic model for the active document. By strongly holding onto it, we ensure + /// that it won't be GC'ed between feature requests from multiple features that care about it. As the active + /// document has the most features running on it continuously, we definitely do not want to drop this. Note: this + /// cached value is only to help with performance. Not with correctness. Importantly, the concept of 'active + /// document' is itself fundamentally racy. That's ok though as we simply want to settle on these semantic models + /// settling into a stable state over time. We don't need to be perfect about it. They are intentionally not + /// locked either as we would only have contention right when switching to a new active document, and we would still + /// latch onto the new document very quickly. + /// + /// + /// It is fine for these fields to never be read. The purpose is simply to keep a strong reference around so that + /// they will not be GC'ed as long as the active document stays the same. + /// +#pragma warning disable IDE0052 // Remove unread private members + private SemanticModel? _activeDocumentSemanticModel; + + /// + private SemanticModel? _activeDocumentNullableDisabledSemanticModel; +#pragma warning restore IDE0052 // Remove unread private members + + internal void OnSemanticModelObtained(DocumentId documentId, SemanticModel semanticModel) + { + var service = this.Services.GetRequiredService(); + + var activeDocumentId = service.TryGetActiveDocument(); + if (activeDocumentId is null) + { + // no active document? then clear out any caches we have. + _activeDocumentSemanticModel = null; + _activeDocumentNullableDisabledSemanticModel = null; + } + else if (activeDocumentId != documentId) + { + // We have an active document, but we just obtained the semantic model for some other doc. Nothing to do + // here, we don't want to cache this. + return; + } + else + { + // Ok. We just obtained the semantic model for the active document. Make a strong reference to it so that + // other features that wake up for this active document are sure to be able to reuse the same one. +#pragma warning disable RSEXPERIMENTAL001 // sym-shipped usage of experimental API + if (semanticModel.NullableAnalysisIsDisabled) + _activeDocumentNullableDisabledSemanticModel = semanticModel; + else + _activeDocumentSemanticModel = semanticModel; +#pragma warning restore RSEXPERIMENTAL001 + } + } +} diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs index e404b3a91c413..cb1591b0eb256 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SourceGeneratedDocumentState.cs @@ -105,7 +105,7 @@ private SourceGeneratedDocumentState( ITextAndVersionSource textSource, SourceText text, LoadTextOptions loadTextOptions, - AsyncLazy treeSource, + ITreeAndVersionSource treeSource, Lazy lazyContentHash, DateTime generationDateTime) : base(languageServices, documentServiceProvider, attributes, options, textSource, loadTextOptions, treeSource) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs index dd1c5d79ec793..080785d0a61fe 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/StateChecksums.cs @@ -486,6 +486,20 @@ public async Task FindAsync( await ChecksumCollection.FindAsync(assetPath, state.AnalyzerConfigDocumentStates, searchingChecksumsLeft, onAssetFound, arg, cancellationToken).ConfigureAwait(false); } } + + public override string ToString() + => $""" + ProjectStateChecksums({ProjectId}) + Info={Info} + CompilationOptions={CompilationOptions} + ParseOptions={ParseOptions} + ProjectReferences={ProjectReferences.Checksum} + MetadataReferences={MetadataReferences.Checksum} + AnalyzerReferences={AnalyzerReferences.Checksum} + Documents={Documents.Checksum} + AdditionalDocuments={AdditionalDocuments.Checksum} + AnalyzerConfigDocuments={AnalyzerConfigDocuments.Checksum} + """; } internal sealed class DocumentStateChecksums( @@ -526,6 +540,9 @@ public async Task FindAsync( onAssetFound(Text, text, arg); } } + + public override string ToString() + => $"DocumentStateChecksums({DocumentId})"; } /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs index 33a799e996aba..63fa8cb9f446d 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState.cs @@ -3,9 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Host; @@ -60,7 +58,7 @@ public TextDocumentState(SolutionServices solutionServices, DocumentInfo info, L info.DocumentServiceProvider, info.Attributes, textAndVersionSource: info.TextLoader != null - ? CreateRecoverableText(info.TextLoader, solutionServices) + ? CreateTextFromLoader(info.TextLoader, PreservationMode.PreserveValue, solutionServices) : CreateStrongText(TextAndVersion.Create(SourceText.From(string.Empty, encoding: null, loadTextOptions.ChecksumAlgorithm), VersionStamp.Default, info.FilePath)), loadTextOptions) { @@ -74,9 +72,6 @@ public TextDocumentState(SolutionServices solutionServices, DocumentInfo info, L private static ITextAndVersionSource CreateStrongText(TextAndVersion text) => new ConstantTextAndVersionSource(text); - private static ITextAndVersionSource CreateStrongText(TextLoader loader) - => new LoadableTextAndVersionSource(loader, cacheResult: true); - private static ITextAndVersionSource CreateRecoverableText(TextAndVersion text, SolutionServices services) { var service = services.GetRequiredService(); @@ -87,18 +82,8 @@ private static ITextAndVersionSource CreateRecoverableText(TextAndVersion text, : new RecoverableTextAndVersion(new ConstantTextAndVersionSource(text), services); } - private static ITextAndVersionSource CreateRecoverableText(TextLoader loader, SolutionServices services) - { - var service = services.GetRequiredService(); - var options = service.Options; - - return options.DisableRecoverableText - ? CreateStrongText(loader) - : new RecoverableTextAndVersion(new LoadableTextAndVersionSource(loader, cacheResult: false), services); - } - - public ITemporaryTextStorageInternal? Storage - => (TextAndVersionSource as RecoverableTextAndVersion)?.Storage; + public ITemporaryStorageTextHandle? StorageHandle + => (TextAndVersionSource as RecoverableTextAndVersion)?.StorageHandle; public bool TryGetText([NotNullWhen(returnValue: true)] out SourceText? text) { @@ -178,13 +163,34 @@ public TextDocumentState UpdateText(SourceText newText, PreservationMode mode) public TextDocumentState UpdateText(TextLoader loader, PreservationMode mode) { // don't blow up on non-text documents. - var newTextSource = mode == PreservationMode.PreserveIdentity - ? CreateStrongText(loader) - : CreateRecoverableText(loader, solutionServices); + var newTextSource = CreateTextFromLoader(loader, mode, this.solutionServices); return UpdateText(newTextSource, mode, incremental: false); } + private static ITextAndVersionSource CreateTextFromLoader(TextLoader loader, PreservationMode mode, SolutionServices solutionServices) + { + var service = solutionServices.GetRequiredService(); + var options = service.Options; + + // If the caller is explicitly stating that identity must be preserved, then we created a source that will load + // from the loader the first time, but then cache that result so that hte same result is *always* returned. + if (mode == PreservationMode.PreserveIdentity || options.DisableRecoverableText) + return new LoadableTextAndVersionSource(loader, cacheResult: true); + + // If the loader asks us to always hold onto it strongly, then we do not want to create a recoverable text + // source here. Instead, we'll go back to the loader each time to get the text. This is useful for when the + // loader knows it can always reconstitute the snapshot exactly as it was before. For example, if the loader + // points at the contents of a memory mapped file in another process. + if (loader.AlwaysHoldStrongly) + return new LoadableTextAndVersionSource(loader, cacheResult: false); + + // Otherwise, we just want to hold onto this loader by value. So we create a loader that will load the + // contents, but not hold onto them strongly, and we wrap it in a recoverable-text that will then take those + // contents and dump it into a memory-mapped-file in this process so that snapshot semantics can be preserved. + return new RecoverableTextAndVersion(new LoadableTextAndVersionSource(loader, cacheResult: false), solutionServices); + } + protected virtual TextDocumentState UpdateText(ITextAndVersionSource newTextSource, PreservationMode mode, bool incremental) { return new TextDocumentState( diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs index 9c1513be2405e..3f646f09e1971 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextDocumentState_Checksum.cs @@ -36,11 +36,9 @@ private async Task ComputeChecksumsAsync(CancellationTok { using (Logger.LogBlock(FunctionId.DocumentState_ComputeChecksumsAsync, FilePath, cancellationToken)) { - var serializer = solutionServices.GetRequiredService(); - var infoChecksum = this.Attributes.Checksum; var serializableText = await SerializableSourceText.FromTextDocumentStateAsync(this, cancellationToken).ConfigureAwait(false); - var textChecksum = serializer.CreateChecksum(serializableText, cancellationToken); + var textChecksum = serializableText.ContentChecksum; return new DocumentStateChecksums(this.Id, infoChecksum, textChecksum); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs b/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs index e771796d35fac..cf625034f68d8 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/TextLoader.cs @@ -31,6 +31,18 @@ public abstract class TextLoader internal virtual string? FilePath => null; + /// + /// if the document that holds onto this loader should do so with a strong reference, versus + /// a reference that will take the contents of this loader and store them in a recoverable form (e.g. a memory + /// mapped file within the same process). This should be used when the underlying data is already stored + /// in a recoverable form somewhere else and it would be wasteful to store another copy. For example, a document + /// that is backed by memory-mapped contents in another process does not need to dump it's content to + /// another memory-mapped file in the process it lives in. It can always recover the text from the original + /// process. + /// + internal virtual bool AlwaysHoldStrongly + => false; + /// /// True if reloads from its original binary representation (e.g. file on disk). /// diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/ITextAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITextAndVersionSource.cs similarity index 83% rename from src/Workspaces/Core/Portable/Workspace/Solution/ITextAndVersionSource.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITextAndVersionSource.cs index ab0bc21554dde..ac7f86ac6a84e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/ITextAndVersionSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITextAndVersionSource.cs @@ -16,6 +16,12 @@ internal interface ITextAndVersionSource /// bool CanReloadText { get; } + /// + /// Retrieves the underlying if that's what this was + /// created from and still has access to. + /// + TextLoader? TextLoader { get; } + bool TryGetValue(LoadTextOptions options, [MaybeNullWhen(false)] out TextAndVersion value); TextAndVersion GetValue(LoadTextOptions options, CancellationToken cancellationToken); Task GetValueAsync(LoadTextOptions options, CancellationToken cancellationToken); diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITreeAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITreeAndVersionSource.cs new file mode 100644 index 0000000000000..75e0d0c670b33 --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/ITreeAndVersionSource.cs @@ -0,0 +1,20 @@ +// 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.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.CodeAnalysis; + +/// +/// Similar to , but for trees. Allows hiding (or introspecting) the details of how +/// a tree is created for a particular document. +/// +internal interface ITreeAndVersionSource +{ + Task GetValueAsync(CancellationToken cancellationToken); + TreeAndVersion GetValue(CancellationToken cancellationToken); + bool TryGetValue([NotNullWhen(true)] out TreeAndVersion? value); +} diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/LoadableTextAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/LoadableTextAndVersionSource.cs similarity index 95% rename from src/Workspaces/Core/Portable/Workspace/Solution/LoadableTextAndVersionSource.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/LoadableTextAndVersionSource.cs index 70417d9b9dbad..28a1f3f6fe04f 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/LoadableTextAndVersionSource.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/LoadableTextAndVersionSource.cs @@ -35,10 +35,10 @@ private sealed class LazyValueWithOptions(LoadableTextAndVersionSource source, L private WeakReference? _weakInstance; private Task LoadAsync(CancellationToken cancellationToken) - => Source.Loader.LoadTextAsync(Options, cancellationToken); + => Source.TextLoader.LoadTextAsync(Options, cancellationToken); private TextAndVersion LoadSynchronously(CancellationToken cancellationToken) - => Source.Loader.LoadTextSynchronously(Options, cancellationToken); + => Source.TextLoader.LoadTextSynchronously(Options, cancellationToken); public bool TryGetValue([MaybeNullWhen(false)] out TextAndVersion value) { @@ -102,13 +102,13 @@ private void UpdateWeakAndStrongReferences_NoLock(TextAndVersion textAndVersion) } } - public readonly TextLoader Loader = loader; + public TextLoader TextLoader { get; } = loader; public readonly bool CacheResult = cacheResult; private LazyValueWithOptions? _lazyValue; public bool CanReloadText - => Loader.CanReloadText; + => TextLoader.CanReloadText; private LazyValueWithOptions GetLazyValue(LoadTextOptions options) { diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.RecoverableText.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.RecoverableText.cs similarity index 100% rename from src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.RecoverableText.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.RecoverableText.cs diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.cs similarity index 88% rename from src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.cs rename to src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.cs index bcc498768b875..7314888e4c9ba 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/RecoverableTextAndVersion.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/RecoverableTextAndVersion.cs @@ -42,8 +42,14 @@ private bool TryGetInitialSourceOrRecoverableText([NotNullWhen(true)] out ITextA return false; } - public ITemporaryTextStorageInternal? Storage - => (_initialSourceOrRecoverableText as RecoverableText)?.Storage; + /// + /// Attempt to return the original loader if we still have it. + /// + public TextLoader? TextLoader + => (_initialSourceOrRecoverableText as ITextAndVersionSource)?.TextLoader; + + public ITemporaryStorageTextHandle? StorageHandle + => (_initialSourceOrRecoverableText as RecoverableText)?.StorageHandle; public bool TryGetValue(LoadTextOptions options, [MaybeNullWhen(false)] out TextAndVersion value) { @@ -137,7 +143,7 @@ private sealed partial class RecoverableText public readonly ITextAndVersionSource? InitialSource; public readonly LoadTextOptions LoadTextOptions; - public ITemporaryTextStorageInternal? _storage; + public ITemporaryStorageTextHandle? _storageHandle; public RecoverableText(ITextAndVersionSource source, TextAndVersion textAndVersion, LoadTextOptions options, SolutionServices services) { @@ -160,37 +166,36 @@ public RecoverableText(ITextAndVersionSource source, TextAndVersion textAndVersi public TextAndVersion ToTextAndVersion(SourceText text) => TextAndVersion.Create(text, Version, LoadDiagnostic); - public ITemporaryTextStorageInternal? Storage => _storage; + public ITemporaryStorageTextHandle? StorageHandle => _storageHandle; private async Task RecoverAsync(CancellationToken cancellationToken) { - Contract.ThrowIfNull(_storage); + Contract.ThrowIfNull(_storageHandle); using (Logger.LogBlock(FunctionId.Workspace_Recoverable_RecoverTextAsync, cancellationToken)) { - return await _storage.ReadTextAsync(cancellationToken).ConfigureAwait(false); + return await _storageHandle.ReadFromTemporaryStorageAsync(cancellationToken).ConfigureAwait(false); } } private SourceText Recover(CancellationToken cancellationToken) { - Contract.ThrowIfNull(_storage); + Contract.ThrowIfNull(_storageHandle); using (Logger.LogBlock(FunctionId.Workspace_Recoverable_RecoverText, cancellationToken)) { - return _storage.ReadText(cancellationToken); + return _storageHandle.ReadFromTemporaryStorage(cancellationToken); } } private async Task SaveAsync(SourceText text, CancellationToken cancellationToken) { - Contract.ThrowIfFalse(_storage == null); // Cannot save more than once + Contract.ThrowIfFalse(_storageHandle == null); // Cannot save more than once - var storage = _storageService.CreateTemporaryTextStorage(); - await storage.WriteTextAsync(text, cancellationToken).ConfigureAwait(false); + var handle = await _storageService.WriteToTemporaryStorageAsync(text, cancellationToken).ConfigureAwait(false); - // make sure write is done before setting _storage field - Interlocked.CompareExchange(ref _storage, storage, null); + // make sure write is done before setting _storageHandle field + Interlocked.CompareExchange(ref _storageHandle, handle, null); // Only set _initialValue to null once writing to the storage service completes fully. If the save did not // complete, we want to keep it around to service future requests. Once we do clear out this value, then diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/SimpleTreeAndVersionSource.cs b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/SimpleTreeAndVersionSource.cs new file mode 100644 index 0000000000000..46be64b1bd3ec --- /dev/null +++ b/src/Workspaces/Core/Portable/Workspace/Solution/VersionSource/SimpleTreeAndVersionSource.cs @@ -0,0 +1,44 @@ +// 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; +using System.Threading; +using System.Threading.Tasks; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis; + +/// +/// Simple implementation of backed by an opaque ."/> +/// +internal sealed class SimpleTreeAndVersionSource : ITreeAndVersionSource +{ + private readonly AsyncLazy _source; + + private SimpleTreeAndVersionSource(AsyncLazy source) + { + _source = source; + } + + public Task GetValueAsync(CancellationToken cancellationToken) + => _source.GetValueAsync(cancellationToken); + + public TreeAndVersion GetValue(CancellationToken cancellationToken) + => _source.GetValue(cancellationToken); + + public bool TryGetValue([NotNullWhen(true)] out TreeAndVersion? value) + => _source.TryGetValue(out value); + + public static SimpleTreeAndVersionSource Create( + Func> asynchronousComputeFunction, + Func? synchronousComputeFunction, TArg arg) + { + return new(AsyncLazy.Create(asynchronousComputeFunction, synchronousComputeFunction, arg)); + } + + public static SimpleTreeAndVersionSource Create(TreeAndVersion source) + => new(AsyncLazy.Create(source)); +} diff --git a/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs b/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs index 4c40c9a8b6565..f948925ecc2f0 100644 --- a/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs +++ b/src/Workspaces/CoreTest/BatchFixAllProviderTests.cs @@ -107,7 +107,7 @@ public override Task RegisterCodeFixesAsync(CodeFixContext context) if (_nested) { - fixes = [CodeAction.Create("Container", fixes.ToImmutableArray(), isInlinable: false)]; + fixes = [CodeAction.Create("Container", [.. fixes], isInlinable: false)]; } foreach (var fix in fixes) diff --git a/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs b/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs index c1a68021dae2b..4c80e4cd799c2 100644 --- a/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs +++ b/src/Workspaces/CoreTest/EditorConfigParsing/EditorConfigFileParserTests.cs @@ -23,7 +23,7 @@ internal static EditorConfigFile CreateParseResults(string e list.Add(parseResult); } - return new EditorConfigFile(editorconfigFilePath, list.ToImmutableArray()); + return new EditorConfigFile(editorconfigFilePath, [.. list]); } [Fact] diff --git a/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs b/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs index 8f28f33ba1510..b5d5d44d9e1dd 100644 --- a/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs +++ b/src/Workspaces/CoreTest/FindAllDeclarationsTests.TestSolutionsAndProject.cs @@ -62,7 +62,7 @@ private static void Verify(SolutionKind workspaceKind, IEnumerable decl private static void VerifyResults(IEnumerable declarations, string[] expectedResults) { declarations = declarations.OrderBy(d => d.ToString()); - expectedResults = expectedResults.OrderBy(r => r).ToArray(); + expectedResults = [.. expectedResults.OrderBy(r => r)]; for (var i = 0; i < expectedResults.Length; i++) { diff --git a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestTemporaryStorageServiceFactory.cs b/src/Workspaces/CoreTest/Host/WorkspaceServices/TestTemporaryStorageServiceFactory.cs deleted file mode 100644 index 21147f43736bd..0000000000000 --- a/src/Workspaces/CoreTest/Host/WorkspaceServices/TestTemporaryStorageServiceFactory.cs +++ /dev/null @@ -1,24 +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.Composition; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.UnitTests.Persistence -{ - [ExportWorkspaceServiceFactory(typeof(ITemporaryStorageServiceInternal), ServiceLayer.Test), Shared, PartNotDiscoverable] - internal sealed class TestTemporaryStorageServiceFactory : IWorkspaceServiceFactory - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public TestTemporaryStorageServiceFactory() - { - } - - public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices) - => TrivialTemporaryStorageService.Instance; - } -} diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs index 0cdc9229c893f..fa04089a158a9 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTestHelpers.cs @@ -7,7 +7,6 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis.Remote.Testing; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.CodeAnalysis.UnitTests.Persistence; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests @@ -17,17 +16,8 @@ internal static class SolutionTestHelpers public static Workspace CreateWorkspace(Type[]? additionalParts = null, TestHost testHost = TestHost.InProcess) => new AdhocWorkspace(FeaturesTestCompositions.Features.AddParts(additionalParts).WithTestHostParts(testHost).GetHostServices()); - public static Workspace CreateWorkspaceWithNormalText() - => CreateWorkspace( - [ - typeof(TestTemporaryStorageServiceFactory) - ]); - - public static Workspace CreateWorkspaceWithRecoverableText() - => CreateWorkspace(); - public static Workspace CreateWorkspaceWithPartialSemantics(TestHost testHost = TestHost.InProcess) - => WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics([typeof(TestTemporaryStorageServiceFactory)], testHost); + => WorkspaceTestUtilities.CreateWorkspaceWithPartialSemantics(testHost: testHost); #nullable disable diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs index db2774e7943e2..ba14298ac442e 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionTests.cs @@ -2216,7 +2216,8 @@ private static async Task ValidateSolutionAndCompilationsAsync(Solution solution { if (solution.ContainsProject(referenced.ProjectId)) { - var referencedMetadata = await solution.CompilationState.GetMetadataReferenceAsync(referenced, solution.GetProjectState(project.Id), CancellationToken.None); + var referencedMetadata = await solution.CompilationState.GetMetadataReferenceAsync( + referenced, solution.GetProjectState(project.Id), includeCrossLanguage: true, CancellationToken.None); Assert.NotNull(referencedMetadata); if (referencedMetadata is CompilationReference compilationReference) { @@ -2651,7 +2652,7 @@ public void TestDocumentChangedOnDiskIsNotObserved() var file = Temp.CreateFile().WriteAllText(text1, Encoding.UTF8); // create a solution that evicts from the cache immediately. - using var workspace = CreateWorkspaceWithRecoverableText(); + using var workspace = CreateWorkspace(); var sol = workspace.CurrentSolution; var pid = ProjectId.CreateNewId(); @@ -3935,7 +3936,7 @@ public void TestUpdateDocumentsOrder() var pid = ProjectId.CreateNewId(); VersionStamp GetVersion() => solution.GetProject(pid).Version; - ImmutableArray GetDocumentIds() => solution.GetProject(pid).DocumentIds.ToImmutableArray(); + ImmutableArray GetDocumentIds() => [.. solution.GetProject(pid).DocumentIds]; ImmutableArray GetSyntaxTrees() { return solution.GetProject(pid).GetCompilationAsync().Result.SyntaxTrees.ToImmutableArray(); @@ -5079,5 +5080,62 @@ public async Task TestFrozenPartialSolutionOtherLanguage() var frozenCompilation = await frozenProject.GetCompilationAsync(); Assert.Null(frozenCompilation); } + + [Theory] + [InlineData(1000)] + [InlineData(2000)] + [InlineData(4000)] + [InlineData(8000)] + public async Task TestLargeLinkedFileChain(int intermediatePullCount) + { + using var workspace = CreateWorkspace(); + + var project1 = workspace.CurrentSolution + .AddProject($"Project1", $"Project1", LanguageNames.CSharp) + .WithParseOptions(CSharpParseOptions.Default.WithPreprocessorSymbols("DEBUG")) + .AddDocument($"Document", SourceText.From("class C { }"), filePath: @"c:\test\Document.cs").Project; + var documentId1 = project1.DocumentIds.Single(); + + // make another project, give a separate set of pp directives, so that we do *not* try to use the sibling + // root (from project1), but instead incrementally parse using the *contents* of the file in project1 again + // our actual tree. This used to stack overflow since we'd create a long chain of incremental parsing steps + // for each edit made to the sibling file. + var project2 = project1.Solution + .AddProject($"Project2", $"Project2", LanguageNames.CSharp) + .WithParseOptions(CSharpParseOptions.Default.WithPreprocessorSymbols("RELEASE")) + .AddDocument($"Document", SourceText.From("class C { }"), filePath: @"c:\test\Document.cs").Project; + var documentId2 = project2.DocumentIds.Single(); + + workspace.SetCurrentSolution( + _ => project2.Solution, + (_, _) => (WorkspaceChangeKind.SolutionAdded, null, null)); + + for (var i = 1; i <= 8000; i++) + { + var lastContents = $"#if true //{new string('.', i)}//"; + workspace.SetCurrentSolution( + old => old.WithDocumentText(documentId1, SourceText.From(lastContents)), + (_, _) => (WorkspaceChangeKind.DocumentChanged, documentId1.ProjectId, documentId1)); + + // Ensure that the first document is fine, and we're not stack overflowing on simply getting the tree + // from it. Do this on a disparate cadence from our pulls of the second document to ensure we are + // testing the case where we haven't necessarily immediately pulled on hte first doc before pulling on + // the second. + if (i % 33 == 0) + { + var document1 = workspace.CurrentSolution.GetRequiredDocument(documentId1); + await document1.GetSyntaxRootAsync(); + } + + if (i % intermediatePullCount == 0) + { + var document2 = workspace.CurrentSolution.GetRequiredDocument(documentId2); + + // Getting the second document should both be fine, and have contents equivalent to what is in the first document. + var root = await document2.GetSyntaxRootAsync(); + Assert.Equal(lastContents, root.ToFullString()); + } + } + } } } diff --git a/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs b/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs index 63807101f0a66..b9e1680f9a148 100644 --- a/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/TryApplyChangesTests.cs @@ -31,7 +31,7 @@ public CustomizedCanApplyWorkspace(ApplyChangesKind[] allowedKinds, Func? canApplyCompilationOptions = null) : base(Host.Mef.MefHostServices.DefaultHost, workspaceKind: nameof(CustomizedCanApplyWorkspace)) { - _allowedKinds = allowedKinds.ToImmutableArray(); + _allowedKinds = [.. allowedKinds]; _canApplyParseOptions = canApplyParseOptions; _canApplyCompilationOptions = canApplyCompilationOptions; diff --git a/src/Workspaces/CoreTest/SyntaxReferenceTests.cs b/src/Workspaces/CoreTest/SyntaxReferenceTests.cs index 2ce0a35a4bb70..6614d2e7f9b26 100644 --- a/src/Workspaces/CoreTest/SyntaxReferenceTests.cs +++ b/src/Workspaces/CoreTest/SyntaxReferenceTests.cs @@ -2,34 +2,26 @@ // 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.Linq; -using Microsoft.CodeAnalysis.Host; -using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; -using Microsoft.CodeAnalysis.UnitTests.Persistence; using Roslyn.Test.Utilities; using Xunit; +using Microsoft.CodeAnalysis.Shared.Extensions; using CS = Microsoft.CodeAnalysis.CSharp; using VB = Microsoft.CodeAnalysis.VisualBasic; +using System.Threading.Tasks; +using System.Threading; namespace Microsoft.CodeAnalysis.UnitTests { [UseExportProvider] [Trait(Traits.Feature, Traits.Features.Workspace)] - public class SyntaxReferenceTests : TestBase + public sealed class SyntaxReferenceTests : TestBase { - private static Workspace CreateWorkspace(Type[] additionalParts = null) - => new AdhocWorkspace(FeaturesTestCompositions.Features.AddParts(additionalParts).GetHostServices()); - - private static Workspace CreateWorkspaceWithRecoverableSyntaxTrees() - => CreateWorkspace( - [ - typeof(TestTemporaryStorageServiceFactory) - ]); + private static Workspace CreateWorkspace() + => new AdhocWorkspace(FeaturesTestCompositions.Features.GetHostServices()); private static Solution AddSingleFileCSharpProject(Solution solution, string source) { @@ -52,16 +44,16 @@ private static Solution AddSingleFileVisualBasicProject(Solution solution, strin } [Fact] - public void TestCSharpReferenceToZeroWidthNode() + public async Task TestCSharpReferenceToZeroWidthNode() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileCSharpProject(workspace.CurrentSolution, @" public class C<> { } "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // this is an expected TypeParameterSyntax with a missing identifier token (it is zero-length w/ an error attached to it) var node = tree.GetRoot().DescendantNodes().OfType().Single(); @@ -75,15 +67,15 @@ public class C<> } [Fact] - public void TestVisualBasicReferenceToZeroWidthNode() + public async Task TestVisualBasicReferenceToZeroWidthNode() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileVisualBasicProject(workspace.CurrentSolution, @" Public Class C(Of ) End Class "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // this is an expected TypeParameterSyntax with a missing identifier token (it is zero-length w/ an error attached to it) var node = tree.GetRoot().DescendantNodes().OfType().Single(); @@ -97,9 +89,9 @@ End Class } [Fact] - public void TestCSharpReferenceToNodeInStructuredTrivia() + public async Task TestCSharpReferenceToNodeInStructuredTrivia() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileCSharpProject(workspace.CurrentSolution, @" #if true || true public class C @@ -107,7 +99,7 @@ public class C } #endif "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var node = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); @@ -120,9 +112,9 @@ public class C } [Fact] - public void TestVisualBasicReferenceToNodeInStructuredTrivia() + public async Task TestVisualBasicReferenceToNodeInStructuredTrivia() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileVisualBasicProject(workspace.CurrentSolution, @" #If True Or True Then Public Class C @@ -130,7 +122,7 @@ End Class #End If "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var node = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); @@ -143,9 +135,9 @@ End Class } [Fact] - public void TestCSharpReferenceToZeroWidthNodeInStructuredTrivia() + public async Task TestCSharpReferenceToZeroWidthNodeInStructuredTrivia() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileCSharpProject(workspace.CurrentSolution, @" #if true || public class C @@ -154,7 +146,7 @@ public class C #endif "); - var tree = solution.Projects.First().Documents.First().GetSyntaxTreeAsync().Result; + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var binary = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); @@ -173,7 +165,7 @@ public class C [Fact] public async System.Threading.Tasks.Task TestVisualBasicReferenceToZeroWidthNodeInStructuredTriviaAsync() { - using var workspace = CreateWorkspaceWithRecoverableSyntaxTrees(); + using var workspace = CreateWorkspace(); var solution = AddSingleFileVisualBasicProject(workspace.CurrentSolution, @" #If (True Or ) Then Public Class C @@ -181,7 +173,7 @@ End Class #End If "); - var tree = await solution.Projects.First().Documents.First().GetSyntaxTreeAsync(); + var tree = await solution.Projects.First().Documents.First().GetRequiredSyntaxTreeAsync(CancellationToken.None); // find binary node that is part of #if directive var binary = tree.GetRoot().DescendantNodes(descendIntoTrivia: true).OfType().First(); diff --git a/src/Workspaces/CoreTest/TestCompositionTests.cs b/src/Workspaces/CoreTest/TestCompositionTests.cs index 13237a0dcb699..24535fa007888 100644 --- a/src/Workspaces/CoreTest/TestCompositionTests.cs +++ b/src/Workspaces/CoreTest/TestCompositionTests.cs @@ -2,21 +2,21 @@ // 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.Collections.Immutable; using Microsoft.CodeAnalysis.Test.Utilities; -using Microsoft.CodeAnalysis.UnitTests.Persistence; using Roslyn.Test.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.UnitTests { + using static SourceGeneratorTelemetryCollectorWorkspaceServiceTests; + public class TestCompositionTests { [Fact] public void FactoryReuse() { - var composition1 = FeaturesTestCompositions.Features.AddParts(typeof(TestErrorReportingService), typeof(TestTemporaryStorageServiceFactory)); - var composition2 = FeaturesTestCompositions.Features.AddParts(typeof(TestTemporaryStorageServiceFactory), typeof(TestErrorReportingService)); + var composition1 = FeaturesTestCompositions.Features.AddParts(typeof(TestErrorReportingService), typeof(TestSourceGeneratorTelemetryCollectorWorkspaceServiceFactory)); + var composition2 = FeaturesTestCompositions.Features.AddParts(typeof(TestSourceGeneratorTelemetryCollectorWorkspaceServiceFactory), typeof(TestErrorReportingService)); Assert.Same(composition1.ExportProviderFactory, composition2.ExportProviderFactory); } diff --git a/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs b/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs index bba69b5e4b499..053cad8235405 100644 --- a/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs +++ b/src/Workspaces/CoreTest/WorkspaceServiceTests/TemporaryStorageServiceTests.cs @@ -18,6 +18,8 @@ namespace Microsoft.CodeAnalysis.UnitTests { + using static TemporaryStorageService; + [UseExportProvider] #if NETCOREAPP [SupportedOSPlatform("windows")] @@ -51,7 +53,6 @@ public void TestTemporaryStorageStream() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var temporaryStorage = service.CreateTemporaryStreamStorage(); using var data = SerializableBytes.CreateWritableStream(); for (var i = 0; i < SharedPools.ByteBufferSize; i++) @@ -59,9 +60,9 @@ public void TestTemporaryStorageStream() data.WriteByte((byte)(i % 2)); } - data.Position = 0; - temporaryStorage.WriteStreamAsync(data).Wait(); - using var result = temporaryStorage.ReadStreamAsync().Result; + var handle = service.WriteToTemporaryStorage(data, CancellationToken.None); + + using var result = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(data.Length, result.Length); for (var i = 0; i < SharedPools.ByteBufferSize; i++) @@ -72,65 +73,15 @@ public void TestTemporaryStorageStream() private static void TestTemporaryStorage(ITemporaryStorageServiceInternal temporaryStorageService, SourceText text) { - // create a temporary storage location - var temporaryStorage = temporaryStorageService.CreateTemporaryTextStorage(); - // write text into it - temporaryStorage.WriteTextAsync(text).Wait(); + var handle = temporaryStorageService.WriteToTemporaryStorage(text, CancellationToken.None); // read text back from it - var text2 = temporaryStorage.ReadTextAsync().Result; + var text2 = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.NotSame(text, text2); Assert.Equal(text.ToString(), text2.ToString()); Assert.Equal(text.Encoding, text2.Encoding); - - temporaryStorage.Dispose(); - } - - [ConditionalFact(typeof(WindowsOnly))] - public void TestTemporaryTextStorageExceptions() - { - using var workspace = new AdhocWorkspace(); - var textFactory = Assert.IsType(workspace.Services.GetService()); - var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryTextStorage(); - - // Nothing has been written yet - Assert.Throws(() => storage.ReadText()); - Assert.Throws(() => storage.ReadTextAsync().Result); - - // write a normal string - var text = SourceText.From(new string(' ', 4096) + "public class A {}"); - storage.WriteTextAsync(text).Wait(); - - // Writing multiple times is not allowed - Assert.Throws(() => storage.WriteText(text)); - Assert.Throws(() => storage.WriteTextAsync(text).Wait()); - } - - [ConditionalFact(typeof(WindowsOnly))] - public void TestTemporaryStreamStorageExceptions() - { - using var workspace = new AdhocWorkspace(); - var textFactory = Assert.IsType(workspace.Services.GetService()); - var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); - - // Nothing has been written yet - Assert.Throws(() => storage.ReadStream(CancellationToken.None)); - Assert.Throws(() => storage.ReadStreamAsync().Result); - - // write a normal stream - var stream = new MemoryStream(); - stream.Write([42], 0, 1); - stream.Position = 0; - storage.WriteStreamAsync(stream).Wait(); - - // Writing multiple times is not allowed - // These should also throw before ever getting to the point where they would look at the null stream arg. - Assert.Throws(() => storage.WriteStream(null!)); - Assert.Throws(() => storage.WriteStreamAsync(null!).Wait()); } [ConditionalFact(typeof(WindowsOnly))] @@ -139,15 +90,15 @@ public void TestZeroLengthStreams() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); // 0 length streams are allowed + TemporaryStorageStreamHandle handle; using (var stream1 = new MemoryStream()) { - storage.WriteStream(stream1); + handle = service.WriteToTemporaryStorage(stream1, CancellationToken.None); } - using (var stream2 = storage.ReadStream(CancellationToken.None)) + using (var stream2 = handle.ReadFromTemporaryStorage(CancellationToken.None)) { Assert.Equal(0, stream2.Length); } @@ -170,19 +121,15 @@ public void TestTemporaryStorageMemoryMappedFileManagement() { for (var j = 1; j < 5; j++) { - using ITemporaryStreamStorageInternal storage1 = service.CreateTemporaryStreamStorage(), - storage2 = service.CreateTemporaryStreamStorage(); - var storage3 = service.CreateTemporaryStreamStorage(); // let the finalizer run for this instance - - storage1.WriteStream(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i - 1)); - storage2.WriteStream(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i)); - storage3.WriteStream(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i + 1)); + var handle1 = service.WriteToTemporaryStorage(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i - 1), CancellationToken.None); + var handle2 = service.WriteToTemporaryStorage(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i), CancellationToken.None); + var handle3 = service.WriteToTemporaryStorage(new MemoryStream(buffer.GetBuffer(), 0, 1024 * i + 1), CancellationToken.None); await Task.Yield(); - using Stream s1 = storage1.ReadStream(), - s2 = storage2.ReadStream(), - s3 = storage3.ReadStream(CancellationToken.None); + using var s1 = handle1.ReadFromTemporaryStorage(CancellationToken.None); + using var s2 = handle2.ReadFromTemporaryStorage(CancellationToken.None); + using var s3 = handle3.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(1024 * i - 1, s1.Length); Assert.Equal(1024 * i, s2.Length); Assert.Equal(1024 * i + 1, s3.Length); @@ -214,20 +161,17 @@ public void TestTemporaryStorageScaling() // Create 4GB of memory mapped files var fileCount = (int)((long)4 * 1024 * 1024 * 1024 / data.Length); - var storageHandles = new List(fileCount); + var storageHandles = new List(fileCount); for (var i = 0; i < fileCount; i++) { - var s = service.CreateTemporaryStreamStorage(); - storageHandles.Add(s); - data.Position = 0; - s.WriteStreamAsync(data).Wait(); + var handle = service.WriteToTemporaryStorage(data, CancellationToken.None); + storageHandles.Add(handle); } for (var i = 0; i < 1024 * 5; i++) { - using var s = storageHandles[i].ReadStreamAsync().Result; + using var s = storageHandles[i].ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(1, s.ReadByte()); - storageHandles[i].Dispose(); } } } @@ -238,7 +182,6 @@ public void StreamTest1() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); using var expected = new MemoryStream(); for (var i = 0; i < 10000; i++) @@ -246,11 +189,10 @@ public void StreamTest1() expected.WriteByte((byte)(i % byte.MaxValue)); } - expected.Position = 0; - storage.WriteStream(expected); + var handle = service.WriteToTemporaryStorage(expected, CancellationToken.None); expected.Position = 0; - using var stream = storage.ReadStream(CancellationToken.None); + using var stream = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(expected.Length, stream.Length); for (var i = 0; i < expected.Length; i++) @@ -265,7 +207,6 @@ public void StreamTest2() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); using var expected = new MemoryStream(); for (var i = 0; i < 10000; i++) @@ -273,11 +214,10 @@ public void StreamTest2() expected.WriteByte((byte)(i % byte.MaxValue)); } - expected.Position = 0; - storage.WriteStream(expected); + var handle = service.WriteToTemporaryStorage(expected, CancellationToken.None); expected.Position = 0; - using var stream = storage.ReadStream(CancellationToken.None); + using var stream = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(expected.Length, stream.Length); var index = 0; @@ -302,7 +242,6 @@ public void StreamTest3() using var workspace = new AdhocWorkspace(); var textFactory = Assert.IsType(workspace.Services.GetService()); var service = Assert.IsType(workspace.Services.GetRequiredService()); - var storage = service.CreateTemporaryStreamStorage(); using var expected = new MemoryStream(); var random = new Random(Environment.TickCount); @@ -315,11 +254,10 @@ public void StreamTest3() expected.WriteByte(value); } - expected.Position = 0; - storage.WriteStream(expected); + var handle = service.WriteToTemporaryStorage(expected, CancellationToken.None); expected.Position = 0; - using var stream = storage.ReadStream(CancellationToken.None); + using var stream = handle.ReadFromTemporaryStorage(CancellationToken.None); Assert.Equal(expected.Length, stream.Length); for (var i = 0; i < expected.Length; i++) diff --git a/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs b/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs index ab8478a29d58e..e9e24ba94cab1 100644 --- a/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs +++ b/src/Workspaces/CoreTestUtilities/Fakes/SimpleAssetSource.cs @@ -26,11 +26,10 @@ public ValueTask GetAssetsAsync( Contract.ThrowIfFalse(map.TryGetValue(checksum, out var data)); using var stream = new MemoryStream(); - using var context = new SolutionReplicationContext(); using (var writer = new ObjectWriter(stream, leaveOpen: true)) { - serializerService.Serialize(data, writer, context, cancellationToken); + serializerService.Serialize(data, writer, cancellationToken); } stream.Position = 0; diff --git a/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs b/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs index da2266620118d..95f12c9ac6ba3 100644 --- a/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs +++ b/src/Workspaces/CoreTestUtilities/MEF/TestComposition.cs @@ -33,9 +33,9 @@ public sealed class TestComposition public CacheKey(ImmutableHashSet assemblies, ImmutableHashSet parts, ImmutableHashSet excludedPartTypes) { - _assemblies = assemblies.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName)).ToImmutableArray(); - _parts = parts.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName)).ToImmutableArray(); - _excludedPartTypes = excludedPartTypes.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName)).ToImmutableArray(); + _assemblies = [.. assemblies.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))]; + _parts = [.. parts.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))]; + _excludedPartTypes = [.. excludedPartTypes.OrderBy((a, b) => string.CompareOrdinal(a.FullName, b.FullName))]; } public override bool Equals(object? obj) diff --git a/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs b/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs index 7f6cfb699835d..e216af1473e25 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/InProcRemoteHostClientProvider.cs @@ -17,6 +17,7 @@ namespace Microsoft.CodeAnalysis.Remote.Testing { +#pragma warning disable CA1416 // Validate platform compatibility internal sealed class InProcRemoteHostClientProvider : IRemoteHostClientProvider, IDisposable { [ExportWorkspaceServiceFactory(typeof(IRemoteHostClientProvider), ServiceLayer.Test), Shared, PartNotDiscoverable] @@ -99,4 +100,5 @@ public void Dispose() public Task TryGetRemoteHostClientAsync(CancellationToken cancellationToken) => Task.FromResult(_lazyClient.Value); } +#pragma warning restore CA1416 // Validate platform compatibility } diff --git a/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs b/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs index 8c42582dd37cc..3ce81ef9f9c1c 100644 --- a/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs +++ b/src/Workspaces/CoreTestUtilities/Remote/TestSerializerService.cs @@ -10,6 +10,7 @@ using System.Collections.Immutable; using System.Composition; using System.Linq; +using System.Runtime.Versioning; using System.Threading; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Host; @@ -19,8 +20,13 @@ using Roslyn.Utilities; using ReferenceEqualityComparer = Roslyn.Utilities.ReferenceEqualityComparer; +#pragma warning disable CA1416 // Validate platform compatibility + namespace Microsoft.CodeAnalysis.UnitTests.Remote { +#if NETCOREAPP + [SupportedOSPlatform("windows")] +#endif internal sealed class TestSerializerService : SerializerService { private static readonly ImmutableDictionary s_wellKnownReferenceNames = ImmutableDictionary.Create(ReferenceEqualityComparer.Instance) @@ -41,7 +47,7 @@ public TestSerializerService(ConcurrentDictionary _sharedTestGeneratorReferences = sharedTestGeneratorReferences; } - public override void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, SolutionReplicationContext context, CancellationToken cancellationToken) + public override void WriteMetadataReferenceTo(MetadataReference reference, ObjectWriter writer, CancellationToken cancellationToken) { var wellKnownReferenceName = s_wellKnownReferenceNames.GetValueOrDefault(reference, null); if (wellKnownReferenceName is not null) @@ -52,7 +58,7 @@ public override void WriteMetadataReferenceTo(MetadataReference reference, Objec else { writer.WriteBoolean(false); - base.WriteMetadataReferenceTo(reference, writer, context, cancellationToken); + base.WriteMetadataReferenceTo(reference, writer, cancellationToken); } } diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs index 2ae90f43f3550..9bddc611e2816 100644 --- a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs @@ -902,7 +902,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)netcore30).HasValue && ((bool?)netcore30).Value) { - references = NetCoreApp.References.ToList(); + references = [.. NetCoreApp.References]; } var netstandard20 = element.Attribute(CommonReferencesNetStandard20Name); @@ -910,7 +910,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)netstandard20).HasValue && ((bool?)netstandard20).Value) { - references = TargetFrameworkUtil.NetStandard20References.ToList(); + references = [.. TargetFrameworkUtil.NetStandard20References]; } var net6 = element.Attribute(CommonReferencesNet6Name); @@ -918,7 +918,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)net6).HasValue && ((bool?)net6).Value) { - references = TargetFrameworkUtil.GetReferences(TargetFramework.Net60).ToList(); + references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net60)]; } var net7 = element.Attribute(CommonReferencesNet7Name); @@ -926,7 +926,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)net7).HasValue && ((bool?)net7).Value) { - references = TargetFrameworkUtil.GetReferences(TargetFramework.Net70).ToList(); + references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net70)]; } var net8 = element.Attribute(CommonReferencesNet8Name); @@ -934,7 +934,7 @@ private IList CreateCommonReferences(XElement element) ((bool?)net8).HasValue && ((bool?)net8).Value) { - references = TargetFrameworkUtil.GetReferences(TargetFramework.Net80).ToList(); + references = [.. TargetFrameworkUtil.GetReferences(TargetFramework.Net80)]; } var mincorlib = element.Attribute(CommonReferencesMinCorlibName); diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs index 309159decf2be..af62f5337cb67 100644 --- a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace`1.cs @@ -638,7 +638,7 @@ internal void InitializeDocuments( Documents.Add(submission.Documents.Single()); } - var solution = CreateSolution(projectNameToTestHostProject.Values.ToArray()); + var solution = CreateSolution([.. projectNameToTestHostProject.Values]); AddTestSolution(solution); foreach (var projectElement in workspaceElement.Elements(ProjectElementName)) diff --git a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs index 6a3c7c067f3ad..ac97eb85341e4 100644 --- a/src/Workspaces/Remote/Core/AbstractAssetProvider.cs +++ b/src/Workspaces/Remote/Core/AbstractAssetProvider.cs @@ -142,8 +142,7 @@ public async Task CreateDocumentInfoAsync( var attributes = await GetAssetAsync(new(AssetPathKind.DocumentAttributes, documentId), attributeChecksum, cancellationToken).ConfigureAwait(false); var serializableSourceText = await GetAssetAsync(new(AssetPathKind.DocumentText, documentId), textChecksum, cancellationToken).ConfigureAwait(false); - var text = await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false); - var textLoader = TextLoader.From(TextAndVersion.Create(text, VersionStamp.Create(), attributes.FilePath)); + var textLoader = serializableSourceText.ToTextLoader(attributes.FilePath); // TODO: do we need version? return new DocumentInfo(attributes, textLoader, documentServiceProvider: null); @@ -190,23 +189,41 @@ public static async Task GetAssetsAsync( await assetProvider.GetAssetsAsync(assetPath, checksumSet, callback, arg, cancellationToken).ConfigureAwait(false); } + /// + /// Returns an array of assets, corresponding to all the checksums found in the given . + /// The assets will be returned in the order corresponding to their checksum in . + /// public static async Task> GetAssetsArrayAsync( this AbstractAssetProvider assetProvider, AssetPath assetPath, ChecksumCollection checksums, CancellationToken cancellationToken) where T : class { + // Note: nothing stops 'checksums' from having multiple identical checksums in it. First, collapse this down to + // a set so we're only asking about unique checksums. using var _1 = PooledHashSet.GetInstance(out var checksumSet); #if NET checksumSet.EnsureCapacity(checksums.Children.Length); #endif checksumSet.AddAll(checksums.Children); - using var _ = ArrayBuilder.GetInstance(checksumSet.Count, out var builder); + using var _2 = PooledDictionary.GetInstance(out var checksumToAsset); await assetProvider.GetAssetHelper().GetAssetsAsync( assetPath, checksumSet, - static (checksum, asset, builder) => builder.Add(asset), - builder, + // Calling .Add here is safe. As checksum-set is a unique set of checksums, we'll never have collions here. + static (checksum, asset, checksumToAsset) => checksumToAsset.Add(checksum, asset), + checksumToAsset, cancellationToken).ConfigureAwait(false); - return builder.ToImmutableAndClear(); + // Note: GetAssetsAsync will only succeed if we actually found all our assets (it crashes otherwise). So we can + // just safely assume we can index into checksumToAsset here. + Contract.ThrowIfTrue(checksumToAsset.Count != checksumSet.Count); + + // The result of GetAssetsArrayAsync wants the returned assets to be in the exact order of the checksums that + // were in 'checksums'. So now fetch the assets in that order, even if we found them in an entirely different + // order. + var result = new FixedSizeArrayBuilder(checksums.Children.Length); + foreach (var checksum in checksums.Children) + result.Add(checksumToAsset[checksum]); + + return result.MoveToImmutable(); } } diff --git a/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs b/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs index 455c83c4177ed..5982dfe6dd27d 100644 --- a/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs +++ b/src/Workspaces/Remote/Core/IRemoteAssetSynchronizationService.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Text; @@ -16,5 +17,22 @@ internal interface IRemoteAssetSynchronizationService /// call into it. /// ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, CancellationToken cancellationToken); - ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextChecksum, IEnumerable textChanges, CancellationToken cancellationToken); + + /// + /// Synchronize the text changes made by a user to a particular document as they are editing it. By sending over + /// the text changes as they happen, we can attempt to 'prime' the remote asset cache with a final that is built based off of retrieving the remote source text with a checksum corresponding + /// to and then applying the to it. Then, when + /// the next remote call comes in for the new solution snapshot, it can hopefully just pluck that text out of the + /// cache without having to sync the entire contents of the file over. + /// + ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextChecksum, ImmutableArray textChanges, CancellationToken cancellationToken); + + /// + /// Synchronize over what the user's current active document is that they're editing. This can then be used by the + /// remote side to help determine which documents are best to strongly hold onto data for, and which should just + /// hold on weakly. Given how much work happens on the active document, this can help avoid the remote side from + /// continually creating and then throwing away that data. + /// + ValueTask SynchronizeActiveDocumentAsync(DocumentId? documentId, CancellationToken cancellationToken); } diff --git a/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs b/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs index ca05cccdec70f..d2d31c20e7efd 100644 --- a/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs +++ b/src/Workspaces/Remote/Core/RemoteHostAssetWriter.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Serialization; +using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Remote; @@ -150,14 +151,11 @@ private async Task ReadAssetsFromChannelAndWriteToPipeAsync(ChannelReader - /// Will be disposed from when the last ref-count to this scope goes - /// away. - /// - public readonly SolutionReplicationContext ReplicationContext = new(); - /// /// Only safe to read write while is held. /// diff --git a/src/Workspaces/Remote/Core/SolutionAssetStorage.cs b/src/Workspaces/Remote/Core/SolutionAssetStorage.cs index 8e87958c46eb3..d5f80f5030c92 100644 --- a/src/Workspaces/Remote/Core/SolutionAssetStorage.cs +++ b/src/Workspaces/Remote/Core/SolutionAssetStorage.cs @@ -111,8 +111,6 @@ private void DecreaseScopeRefCount(Scope scope) // Last ref went away, update our maps while under the lock, then cleanup its context data outside of the lock. _checksumToScope.Remove(solutionChecksum); } - - scope.ReplicationContext.Dispose(); } internal TestAccessor GetTestAccessor() diff --git a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs index 84b0ea0dd5915..5c23f42a6faa0 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/RemoteWorkspace.SolutionCreator.cs @@ -617,13 +617,14 @@ private async Task UpdateDocumentAsync( { var serializableSourceText = await _assetProvider.GetAssetAsync( assetPath: document.Id, newDocumentChecksums.textChecksum, cancellationToken).ConfigureAwait(false); - var sourceText = await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false); + var loader = serializableSourceText.ToTextLoader(document.FilePath); + var mode = PreservationMode.PreserveValue; document = document.Kind switch { - TextDocumentKind.Document => document.Project.Solution.WithDocumentText(document.Id, sourceText).GetDocument(document.Id)!, - TextDocumentKind.AnalyzerConfigDocument => document.Project.Solution.WithAnalyzerConfigDocumentText(document.Id, sourceText).GetAnalyzerConfigDocument(document.Id)!, - TextDocumentKind.AdditionalDocument => document.Project.Solution.WithAdditionalDocumentText(document.Id, sourceText).GetAdditionalDocument(document.Id)!, + TextDocumentKind.Document => document.Project.Solution.WithDocumentTextLoader(document.Id, loader, mode).GetRequiredDocument(document.Id), + TextDocumentKind.AnalyzerConfigDocument => document.Project.Solution.WithAnalyzerConfigDocumentTextLoader(document.Id, loader, mode).GetRequiredAnalyzerConfigDocument(document.Id), + TextDocumentKind.AdditionalDocument => document.Project.Solution.WithAdditionalDocumentTextLoader(document.Id, loader, mode).GetRequiredAdditionalDocument(document.Id), _ => throw ExceptionUtilities.UnexpectedValue(document.Kind), }; } @@ -684,7 +685,7 @@ private async Task ValidateChecksumAsync( var workspace = new AdhocWorkspace(_hostServices); workspace.AddSolution(solutionInfo); - await TestUtils.AssertChecksumsAsync(_assetProvider, checksumFromRequest, workspace.CurrentSolution, incrementalSolutionBuilt).ConfigureAwait(false); + await TestUtils.AssertChecksumsAsync(_assetProvider, checksumFromRequest, workspace.CurrentSolution, incrementalSolutionBuilt, projectConeId).ConfigureAwait(false); } #endif } diff --git a/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs b/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs index 11067ca427033..b409b3b8e28db 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/SolutionAssetCache.cs @@ -267,5 +267,14 @@ public Entry(object @object) Object = @object; } } + + public TestAccessor GetTestAccessor() + => new(this); + + public readonly struct TestAccessor(SolutionAssetCache cache) + { + public void Clear() + => cache._assets.Clear(); + } } diff --git a/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs b/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs index 27047d2158e5b..ada4f5f1be1c1 100644 --- a/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs +++ b/src/Workspaces/Remote/ServiceHub/Host/TestUtils.cs @@ -40,43 +40,44 @@ internal static async Task AssertChecksumsAsync( AssetProvider assetService, Checksum checksumFromRequest, Solution solutionFromScratch, - Solution incrementalSolutionBuilt) + Solution incrementalSolutionBuilt, + ProjectId? projectConeId) { #if DEBUG var sb = new StringBuilder(); var allChecksumsFromRequest = await GetAllChildrenChecksumsAsync(checksumFromRequest).ConfigureAwait(false); - var assetMapFromNewSolution = await solutionFromScratch.GetAssetMapAsync(CancellationToken.None).ConfigureAwait(false); - var assetMapFromIncrementalSolution = await incrementalSolutionBuilt.GetAssetMapAsync(CancellationToken.None).ConfigureAwait(false); + var assetMapFromNewSolution = await solutionFromScratch.GetAssetMapAsync(projectConeId, CancellationToken.None).ConfigureAwait(false); + var assetMapFromIncrementalSolution = await incrementalSolutionBuilt.GetAssetMapAsync(projectConeId, CancellationToken.None).ConfigureAwait(false); // check 4 things // 1. first see if we create new solution from scratch, it works as expected (indicating a bug in incremental update) var mismatch1 = assetMapFromNewSolution.Where(p => !allChecksumsFromRequest.Contains(p.Key)).ToList(); - AppendMismatch(mismatch1, "assets only in new solutoin but not in the request", sb); + AppendMismatch(mismatch1, "Assets only in new solution but not in the request", sb); // 2. second check what items is mismatching for incremental solution var mismatch2 = assetMapFromIncrementalSolution.Where(p => !allChecksumsFromRequest.Contains(p.Key)).ToList(); - AppendMismatch(mismatch2, "assets only in the incremental solution but not in the request", sb); + AppendMismatch(mismatch2, "Assets only in the incremental solution but not in the request", sb); // 3. check whether solution created from scratch and incremental one have any mismatch var mismatch3 = assetMapFromNewSolution.Where(p => !assetMapFromIncrementalSolution.ContainsKey(p.Key)).ToList(); - AppendMismatch(mismatch3, "assets only in new solution but not in incremental solution", sb); + AppendMismatch(mismatch3, "Assets only in new solution but not in incremental solution", sb); var mismatch4 = assetMapFromIncrementalSolution.Where(p => !assetMapFromNewSolution.ContainsKey(p.Key)).ToList(); - AppendMismatch(mismatch4, "assets only in incremental solution but not in new solution", sb); + AppendMismatch(mismatch4, "Assets only in incremental solution but not in new solution", sb); // 4. see what item is missing from request var mismatch5 = await GetAssetFromAssetServiceAsync(allChecksumsFromRequest.Except(assetMapFromNewSolution.Keys)).ConfigureAwait(false); - AppendMismatch(mismatch5, "assets only in the request but not in new solution", sb); + AppendMismatch(mismatch5, "Assets only in the request but not in new solution", sb); var mismatch6 = await GetAssetFromAssetServiceAsync(allChecksumsFromRequest.Except(assetMapFromIncrementalSolution.Keys)).ConfigureAwait(false); - AppendMismatch(mismatch6, "assets only in the request but not in incremental solution", sb); + AppendMismatch(mismatch6, "Assets only in the request but not in incremental solution", sb); var result = sb.ToString(); if (result.Length > 0) { Logger.Log(FunctionId.SolutionCreator_AssetDifferences, result); - Debug.Fail("Differences detected in solution checksum: " + result); + Debug.Fail($"Differences detected in solution checksum (ProjectId={projectConeId}):\r\n{result}"); } return; @@ -154,10 +155,10 @@ private static void AddAllTo(DocumentStateChecksums documentStateChecksums, Hash /// create checksum to corresponding object map from solution this map should contain every parts of solution /// that can be used to re-create the solution back /// - public static async Task> GetAssetMapAsync(this Solution solution, CancellationToken cancellationToken) + public static async Task> GetAssetMapAsync(this Solution solution, ProjectId? projectConeId, CancellationToken cancellationToken) { var map = new Dictionary(); - await solution.AppendAssetMapAsync(map, cancellationToken).ConfigureAwait(false); + await solution.AppendAssetMapAsync(map, projectConeId, cancellationToken).ConfigureAwait(false); return map; } diff --git a/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs b/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs index 5d27541525254..c86c7482b169d 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/AssetSynchronization/RemoteAssetSynchronizationService.cs @@ -3,112 +3,113 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Internal.Log; using Microsoft.CodeAnalysis.Serialization; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; using RoslynLogger = Microsoft.CodeAnalysis.Internal.Log.Logger; -namespace Microsoft.CodeAnalysis.Remote +namespace Microsoft.CodeAnalysis.Remote; + +/// +/// This service is used by the SolutionChecksumUpdater to proactively update the solution snapshot in the +/// out-of-process workspace. We do this to limit the amount of time required to synchronize a solution over after +/// an edit once a feature is asking for a snapshot. +/// +internal sealed class RemoteAssetSynchronizationService(in BrokeredServiceBase.ServiceConstructionArguments arguments) + : BrokeredServiceBase(in arguments), IRemoteAssetSynchronizationService { - /// - /// This service is used by the SolutionChecksumUpdater to proactively update the solution snapshot in the - /// out-of-process workspace. We do this to limit the amount of time required to synchronize a solution over after - /// an edit once a feature is asking for a snapshot. - /// - internal sealed class RemoteAssetSynchronizationService : BrokeredServiceBase, IRemoteAssetSynchronizationService + internal sealed class Factory : FactoryBase { - internal sealed class Factory : FactoryBase - { - protected override IRemoteAssetSynchronizationService CreateService(in ServiceConstructionArguments arguments) - => new RemoteAssetSynchronizationService(in arguments); - } + protected override IRemoteAssetSynchronizationService CreateService(in ServiceConstructionArguments arguments) + => new RemoteAssetSynchronizationService(in arguments); + } - public RemoteAssetSynchronizationService(in ServiceConstructionArguments arguments) - : base(in arguments) + public ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, CancellationToken cancellationToken) + { + return RunServiceAsync(async cancellationToken => { - } + using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizePrimaryWorkspaceAsync, Checksum.GetChecksumLogInfo, solutionChecksum, cancellationToken)) + { + var workspace = GetWorkspace(); + var assetProvider = workspace.CreateAssetProvider(solutionChecksum, WorkspaceManager.SolutionAssetCache, SolutionAssetSource); + await workspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, cancellationToken).ConfigureAwait(false); + } + }, cancellationToken); + } - public ValueTask SynchronizePrimaryWorkspaceAsync(Checksum solutionChecksum, CancellationToken cancellationToken) + public ValueTask SynchronizeActiveDocumentAsync(DocumentId? documentId, CancellationToken cancellationToken) + { + var documentTrackingService = GetWorkspace().Services.GetRequiredService() as RemoteDocumentTrackingService; + documentTrackingService?.SetActiveDocument(documentId); + return ValueTaskFactory.CompletedTask; + } + + public ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextChecksum, ImmutableArray textChanges, CancellationToken cancellationToken) + { + return RunServiceAsync(async cancellationToken => { - return RunServiceAsync(async cancellationToken => + var workspace = GetWorkspace(); + + using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizeTextAsync, Checksum.GetChecksumLogInfo, baseTextChecksum, cancellationToken)) { - using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizePrimaryWorkspaceAsync, Checksum.GetChecksumLogInfo, solutionChecksum, cancellationToken)) + // Try to get the text associated with baseTextChecksum + var text = await TryGetSourceTextAsync(WorkspaceManager, workspace, documentId, baseTextChecksum, cancellationToken).ConfigureAwait(false); + if (text == null) { - var workspace = GetWorkspace(); - var assetProvider = workspace.CreateAssetProvider(solutionChecksum, WorkspaceManager.SolutionAssetCache, SolutionAssetSource); - await workspace.UpdatePrimaryBranchSolutionAsync(assetProvider, solutionChecksum, cancellationToken).ConfigureAwait(false); + // it won't bring in base text if it is not there already. + // text needed will be pulled in when there is request + return; } - }, cancellationToken); - } - public ValueTask SynchronizeTextAsync(DocumentId documentId, Checksum baseTextChecksum, IEnumerable textChanges, CancellationToken cancellationToken) - { - return RunServiceAsync(async cancellationToken => - { - var workspace = GetWorkspace(); - - using (RoslynLogger.LogBlock(FunctionId.RemoteHostService_SynchronizeTextAsync, Checksum.GetChecksumLogInfo, baseTextChecksum, cancellationToken)) - { - var serializer = workspace.Services.GetRequiredService(); + // Now attempt to manually apply the edit, producing the new forked text. Store that directly in + // the asset cache so that future calls to retrieve it can do so quickly, without synchronizing over + // the entire document. + var newText = text.WithChanges(textChanges); + var newSerializableText = new SerializableSourceText(newText, newText.GetContentHash()); - // Try to get the text associated with baseTextChecksum - var text = await TryGetSourceTextAsync(WorkspaceManager, workspace, documentId, baseTextChecksum, cancellationToken).ConfigureAwait(false); - if (text == null) - { - // it won't bring in base text if it is not there already. - // text needed will be pulled in when there is request - return; - } + WorkspaceManager.SolutionAssetCache.GetOrAdd(newSerializableText.ContentChecksum, newSerializableText); + } - // Now attempt to manually apply the edit, producing the new forked text. Store that directly in - // the asset cache so that future calls to retrieve it can do so quickly, without synchronizing over - // the entire document. - var newText = text.WithChanges(textChanges); - var newSerializableText = new SerializableSourceText(newText, newText.GetContentHash()); - var newChecksum = serializer.CreateChecksum(newSerializableText, cancellationToken); + return; - WorkspaceManager.SolutionAssetCache.GetOrAdd(newChecksum, newSerializableText); + async static Task TryGetSourceTextAsync( + RemoteWorkspaceManager workspaceManager, + Workspace workspace, + DocumentId documentId, + Checksum baseTextChecksum, + CancellationToken cancellationToken) + { + // check the cheap and fast one first. + // see if the cache has the source text + if (workspaceManager.SolutionAssetCache.TryGetAsset(baseTextChecksum, out var serializableSourceText)) + { + return await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false); } - return; - - async static Task TryGetSourceTextAsync( - RemoteWorkspaceManager workspaceManager, - Workspace workspace, - DocumentId documentId, - Checksum baseTextChecksum, - CancellationToken cancellationToken) + // do slower one + // check whether existing solution has it + var document = workspace.CurrentSolution.GetDocument(documentId); + if (document == null) { - // check the cheap and fast one first. - // see if the cache has the source text - if (workspaceManager.SolutionAssetCache.TryGetAsset(baseTextChecksum, out var serializableSourceText)) - { - return await serializableSourceText.GetTextAsync(cancellationToken).ConfigureAwait(false); - } - - // do slower one - // check whether existing solution has it - var document = workspace.CurrentSolution.GetDocument(documentId); - if (document == null) - { - return null; - } - - // check checksum whether it is there. - // since we lazily synchronize whole solution (SynchronizePrimaryWorkspaceAsync) when things are idle, - // soon or later this will get hit even if text changes got out of sync due to issues in VS side - // such as file is first opened and there is no SourceText in memory yet. - if (!document.State.TryGetStateChecksums(out var state) || - !state.Text.Equals(baseTextChecksum)) - { - return null; - } + return null; + } - return await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + // check checksum whether it is there. + // since we lazily synchronize whole solution (SynchronizePrimaryWorkspaceAsync) when things are idle, + // soon or later this will get hit even if text changes got out of sync due to issues in VS side + // such as file is first opened and there is no SourceText in memory yet. + if (!document.State.TryGetStateChecksums(out var state) || + !state.Text.Equals(baseTextChecksum)) + { + return null; } - }, cancellationToken); - } + + return await document.GetValueTextAsync(cancellationToken).ConfigureAwait(false); + } + }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs b/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs index 0b331f550e052..1b405b7c66a44 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/AsynchronousOperationListener/RemoteAsynchronousOperationListenerService.cs @@ -41,7 +41,7 @@ public ValueTask IsCompletedAsync(ImmutableArray featureNames, Can var exportProvider = workspace.Services.SolutionServices.ExportProvider; var listenerProvider = exportProvider.GetExports().Single().Value; - return new ValueTask(!listenerProvider.HasPendingWaiter(featureNames.ToArray())); + return new ValueTask(!listenerProvider.HasPendingWaiter([.. featureNames])); }, cancellationToken); } @@ -53,7 +53,7 @@ public ValueTask ExpeditedWaitAsync(ImmutableArray featureNames, Cancell var exportProvider = workspace.Services.SolutionServices.ExportProvider; var listenerProvider = exportProvider.GetExports().Single().Value; - await listenerProvider.WaitAllAsync(workspace, featureNames.ToArray()).ConfigureAwait(false); + await listenerProvider.WaitAllAsync(workspace, [.. featureNames]).ConfigureAwait(false); }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/RemoteDocumentTrackingService.cs b/src/Workspaces/Remote/ServiceHub/Services/RemoteDocumentTrackingService.cs new file mode 100644 index 0000000000000..4a83c398ac9b6 --- /dev/null +++ b/src/Workspaces/Remote/ServiceHub/Services/RemoteDocumentTrackingService.cs @@ -0,0 +1,32 @@ +// 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.Host.Mef; + +namespace Microsoft.CodeAnalysis.Remote; + +[ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Host), Shared] +[method: ImportingConstructor] +[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] +internal sealed class RemoteDocumentTrackingService() : IDocumentTrackingService +{ + private DocumentId? _activeDocument; + + public event EventHandler? ActiveDocumentChanged; + + public ImmutableArray GetVisibleDocuments() + => []; + + public DocumentId? TryGetActiveDocument() + => _activeDocument; + + internal void SetActiveDocument(DocumentId? documentId) + { + _activeDocument = documentId; + ActiveDocumentChanged?.Invoke(this, documentId); + } +} diff --git a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs index 2db59acf92969..e593b1d8928de 100644 --- a/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs +++ b/src/Workspaces/Remote/ServiceHub/Services/SemanticClassification/RemoteSemanticClassificationService.cs @@ -52,7 +52,7 @@ await AbstractClassificationService.AddClassificationsInCurrentProcessAsync( _workQueue.AddWork((document, type, options)); } - return SerializableClassifiedSpans.Dehydrate(temp.ToImmutableArray()); + return SerializableClassifiedSpans.Dehydrate([.. temp]); }, cancellationToken); } } diff --git a/src/Workspaces/Remote/ServiceHub/Services/ServiceHubDocumentTrackingService.cs b/src/Workspaces/Remote/ServiceHub/Services/ServiceHubDocumentTrackingService.cs deleted file mode 100644 index 9f05d82f89f4f..0000000000000 --- a/src/Workspaces/Remote/ServiceHub/Services/ServiceHubDocumentTrackingService.cs +++ /dev/null @@ -1,49 +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 System.Diagnostics; -using Microsoft.CodeAnalysis.ErrorReporting; -using Microsoft.CodeAnalysis.Host.Mef; - -namespace Microsoft.CodeAnalysis.Remote -{ - [ExportWorkspaceService(typeof(IDocumentTrackingService), ServiceLayer.Host)] - [Shared] - internal sealed class ServiceHubDocumentTrackingService : IDocumentTrackingService - { - [ImportingConstructor] - [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] - public ServiceHubDocumentTrackingService() - { - } - - public bool SupportsDocumentTracking => false; - - public event EventHandler ActiveDocumentChanged { add { } remove { } } - - public ImmutableArray GetVisibleDocuments() - { - Fail("Code should not be attempting to obtain visible documents from a stateless remote invocation."); - return []; - } - - public DocumentId? TryGetActiveDocument() - { - Fail("Code should not be attempting to obtain active document from a stateless remote invocation."); - return null; - } - - private static void Fail(string message) - { - // assert in debug builds to hopefully catch problems in CI - Debug.Fail(message); - - // record NFW to see who violates contract. - FatalError.ReportAndCatch(new InvalidOperationException(message)); - } - } -} diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs index e18cb1274e9d3..2e538e86dd901 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/CSharp/Indentation/CSharpSmartTokenFormatter.cs @@ -58,8 +58,7 @@ public IList FormatRange( // Exception 2: Similar behavior for do-while if (common.ContainsDiagnostics && !CloseBraceOfTryOrDoBlock(endToken)) { - smartTokenformattingRules = ImmutableArray.Empty.Add( - new NoLineChangeFormattingRule()).AddRange(_formattingRules); + smartTokenformattingRules = [new NoLineChangeFormattingRule(), .. _formattingRules]; } var formatter = CSharpSyntaxFormatting.Instance; diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs index e1509de06a403..da3c86ea8f721 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Extensions/INamedTypeSymbolExtensions.cs @@ -393,7 +393,7 @@ private static ImmutableArray GetInterfacesToImplement( cancellationToken.ThrowIfCancellationRequested(); interfacesToImplement.RemoveRange(alreadyImplementedInterfaces); - return interfacesToImplement.ToImmutableArray(); + return [.. interfacesToImplement]; } private static ImmutableArray GetUnimplementedMembers( @@ -560,7 +560,7 @@ public static ImmutableArray GetOverridableMembers( RemoveNonOverriddableMembers(result, containingType, cancellationToken); } - return result.Keys.OrderBy(s => result[s]).ToImmutableArray(); + return [.. result.Keys.OrderBy(s => result[s])]; static void RemoveOverriddenMembers( Dictionary result, INamedTypeSymbol containingType, CancellationToken cancellationToken) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs index c5cf608238719..fa6a76cf05a04 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.AnalysisData.cs @@ -86,7 +86,7 @@ protected AnalysisData() /// public SymbolUsageResult ToResult() => new(SymbolsWriteBuilder.ToImmutableDictionary(), - SymbolsReadBuilder.ToImmutableHashSet()); + [.. SymbolsReadBuilder]); public BasicBlockAnalysisData AnalyzeLocalFunctionInvocation(IMethodSymbol localFunction, CancellationToken cancellationToken) { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs index 1a25bc7db5577..8b0b6e1b3b97a 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/FlowAnalysis/SymbolUsageAnalysis/SymbolUsageAnalysis.DataFlowAnalyzer.FlowGraphAnalysisData.cs @@ -196,7 +196,7 @@ private static ImmutableHashSet GetCapturedLocals(ControlFlowGraph } } - return builder.ToImmutableHashSet(); + return [.. builder]; } public BasicBlockAnalysisData GetBlockAnalysisData(BasicBlock basicBlock) @@ -600,7 +600,7 @@ public override bool TryGetDelegateInvocationTargets(IOperation write, out Immut // Attempts to return potential lamba/local function delegate invocation targets for the given write. if (_reachingDelegateCreationTargets.TryGetValue(write, out var targetsBuilder)) { - targets = targetsBuilder.ToImmutableHashSet(); + targets = [.. targetsBuilder]; return true; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs index 99aeca8efb44c..4bf5a87ee261d 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Services/FileBannerFacts/AbstractFileBannerFacts.cs @@ -157,7 +157,7 @@ public TSyntaxNode GetNodeWithoutLeadingBannerAndPreprocessorDirectives [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int lineNumber = 0) where T : class? + public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) where T : class? { if (value is null) { - Fail("Unexpected null", lineNumber); + Fail("Unexpected null", lineNumber, filePath); } } @@ -35,11 +36,11 @@ public static void ThrowIfNull([NotNull] T value, [CallerLineNumber] int line /// all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lineNumber = 0) where T : struct + public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) where T : struct { if (value is null) { - Fail("Unexpected null", lineNumber); + Fail("Unexpected null", lineNumber, filePath); } } @@ -48,11 +49,11 @@ public static void ThrowIfNull([NotNull] T? value, [CallerLineNumber] int lin /// all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, string message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfNull([NotNull] T value, string message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (value is null) { - Fail(message, lineNumber); + Fail(message, lineNumber, filePath); } } @@ -61,11 +62,11 @@ public static void ThrowIfNull([NotNull] T value, string message, [CallerLine /// all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerArgument("value")] ThrowIfNullInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerArgument("value")] ThrowIfNullInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (value is null) { - Fail(message.GetFormattedText(), lineNumber); + Fail(message.GetFormattedText(), lineNumber, filePath); } } @@ -74,11 +75,11 @@ public static void ThrowIfNull([NotNull] T value, [InterpolatedStringHandlerA /// in all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (!condition) { - Fail("Unexpected false", lineNumber); + Fail("Unexpected false", lineNumber, filePath); } } @@ -87,11 +88,11 @@ public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool co /// in all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, string message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (!condition) { - Fail(message, lineNumber); + Fail(message, lineNumber, filePath); } } @@ -100,11 +101,11 @@ public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool co /// in all builds /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfFalseInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfFalseInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (!condition) { - Fail(message.GetFormattedText(), lineNumber); + Fail(message.GetFormattedText(), lineNumber, filePath); } } @@ -113,11 +114,11 @@ public static void ThrowIfFalse([DoesNotReturnIf(parameterValue: false)] bool co /// all builds. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (condition) { - Fail("Unexpected true", lineNumber); + Fail("Unexpected true", lineNumber, filePath); } } @@ -126,11 +127,11 @@ public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool cond /// all builds. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, string message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, string message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (condition) { - Fail(message, lineNumber); + Fail(message, lineNumber, filePath); } } @@ -139,17 +140,20 @@ public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool cond /// all builds. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfTrueInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0) + public static void ThrowIfTrue([DoesNotReturnIf(parameterValue: true)] bool condition, [InterpolatedStringHandlerArgument("condition")] ThrowIfTrueInterpolatedStringHandler message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) { if (condition) { - Fail(message.GetFormattedText(), lineNumber); + Fail(message.GetFormattedText(), lineNumber, filePath); } } [DebuggerHidden] [DoesNotReturn] [MethodImpl(MethodImplOptions.NoInlining)] - public static void Fail(string message = "Unexpected", [CallerLineNumber] int lineNumber = 0) - => throw new InvalidOperationException($"{message} - line {lineNumber}"); + public static void Fail(string message = "Unexpected", [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string? filePath = null) + { + var fileName = filePath is null ? null : Path.GetFileName(filePath); + throw new InvalidOperationException($"{message} - file {fileName} line {lineNumber}"); + } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs index 07dbf18c1be31..5754d73ddfbc0 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/PathMetadataUtilities.cs @@ -71,6 +71,6 @@ public static ImmutableArray BuildFoldersFromNamespace(string? @namespac } var parts = @namespace.Split(NamespaceSeparatorArray, options: StringSplitOptions.RemoveEmptyEntries); - return parts.ToImmutableArray(); + return [.. parts]; } } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs index 9886c1975190c..83d4ad3474067 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/RestrictedInternalsVisibleToAttribute.cs @@ -10,5 +10,5 @@ namespace System.Runtime.CompilerServices; internal sealed class RestrictedInternalsVisibleToAttribute(string assemblyName, params string[] allowedNamespaces) : Attribute { public string AssemblyName { get; } = assemblyName; - public ImmutableArray AllowedNamespaces { get; } = allowedNamespaces.ToImmutableArray(); + public ImmutableArray AllowedNamespaces { get; } = [.. allowedNamespaces]; } diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs index d70dc269c854c..8df5ceefaca7f 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/CSharp/CodeGeneration/CSharpCodeGenerationService.cs @@ -501,7 +501,7 @@ public override TDeclarationNode AddStatements( } else if (destinationMember is AccessorDeclarationSyntax accessorDeclaration) { - return (accessorDeclaration.Body == null) ? destinationMember : Cast(accessorDeclaration.AddBodyStatements(StatementGenerator.GenerateStatements(statements).ToArray())); + return (accessorDeclaration.Body == null) ? destinationMember : Cast(accessorDeclaration.AddBodyStatements([.. StatementGenerator.GenerateStatements(statements)])); } else if (destinationMember is CompilationUnitSyntax compilationUnit && info.Context.BestLocation is null) { @@ -520,7 +520,7 @@ public override TDeclarationNode AddStatements( // statement container. If the global statement is not already a block, create a block which can hold // both the original statement and any new statements we are adding to it. var block = statement as BlockSyntax ?? Block(statement); - return Cast(block.AddStatements(StatementGenerator.GenerateStatements(statements).ToArray())); + return Cast(block.AddStatements([.. StatementGenerator.GenerateStatements(statements)])); } else { diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs index 09b58b5e6a7fe..3839eb370ebbc 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/CodeGenerationHelpers.cs @@ -71,7 +71,7 @@ public static void GetNameAndInnermostNamespace( break; } - name = string.Join(".", names.ToArray()); + name = string.Join(".", [.. names]); } else { @@ -336,7 +336,7 @@ public static int GetInsertionIndex( // The list was grouped (by type, staticness, accessibility). Try to find a location // to put the new declaration into. - var result = Array.BinarySearch(declarationList.ToArray(), declaration, comparerWithoutNameCheck); + var result = Array.BinarySearch([.. declarationList], declaration, comparerWithoutNameCheck); var desiredGroupIndex = result < 0 ? ~result : result; Debug.Assert(desiredGroupIndex >= 0); Debug.Assert(desiredGroupIndex <= declarationList.Count); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs index 5d525a7b31c10..45c058621bda5 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractMethodSymbol.cs @@ -109,7 +109,7 @@ public virtual ImmutableArray ReturnTypeCustomModifiers public ImmutableArray UnmanagedCallingConventionTypes => []; public IMethodSymbol Construct(params ITypeSymbol[] typeArguments) - => new CodeGenerationConstructedMethodSymbol(this, typeArguments.ToImmutableArray()); + => new CodeGenerationConstructedMethodSymbol(this, [.. typeArguments]); public IMethodSymbol Construct(ImmutableArray typeArguments, ImmutableArray typeArgumentNullableAnnotations) => new CodeGenerationConstructedMethodSymbol(this, typeArguments); diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs index 5b06942521e2d..445328ff50423 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/CodeGeneration/Symbols/CodeGenerationAbstractNamedTypeSymbol.cs @@ -63,7 +63,7 @@ public INamedTypeSymbol Construct(params ITypeSymbol[] typeArguments) } return new CodeGenerationConstructedNamedTypeSymbol( - ConstructedFrom, typeArguments.ToImmutableArray(), this.TypeMembers); + ConstructedFrom, [.. typeArguments], this.TypeMembers); } public INamedTypeSymbol Construct(ImmutableArray typeArguments, ImmutableArray typeArgumentNullableAnnotations) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs index 2d2c83b236f21..d7d5fa8da2911 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Extensions/DocumentExtensions.cs @@ -41,6 +41,21 @@ public static async ValueTask GetRequiredSemanticModelAsync(this return semanticModel ?? throw new InvalidOperationException(string.Format(WorkspaceExtensionsResources.SyntaxTree_is_required_to_accomplish_the_task_but_is_not_supported_by_document_0, document.Name)); } +#if !CODE_STYLE + + public static async ValueTask GetRequiredNullableDisabledSemanticModelAsync(this Document document, CancellationToken cancellationToken) + { + if (document.TryGetNullableDisabledSemanticModel(out var semanticModel)) + return semanticModel; + +#pragma warning disable RSEXPERIMENTAL001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + semanticModel = await document.GetSemanticModelAsync(SemanticModelOptions.DisableNullableAnalysis, cancellationToken).ConfigureAwait(false); +#pragma warning restore RSEXPERIMENTAL001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + return semanticModel ?? throw new InvalidOperationException(string.Format(WorkspaceExtensionsResources.SyntaxTree_is_required_to_accomplish_the_task_but_is_not_supported_by_document_0, document.Name)); + } + +#endif + public static async ValueTask GetRequiredSyntaxTreeAsync(this Document document, CancellationToken cancellationToken) { if (document.TryGetSyntaxTree(out var syntaxTree)) diff --git a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs index fcf86c82b58ca..bc4197cc204c1 100644 --- a/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs +++ b/src/Workspaces/SharedUtilitiesAndExtensions/Workspace/Core/Helpers/MefHostServicesHelpers.cs @@ -27,7 +27,7 @@ public static ImmutableArray LoadNearbyAssemblies(IEnumerable } } - return assemblies.ToImmutableArray(); + return [.. assemblies]; } private static Assembly TryLoadNearbyAssembly(string assemblySimpleName)