-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Communicate with OOP process to determine if an AnalyzerReference has analyzers or source generators. #74810
Changes from 14 commits
d7c8b19
db32141
b563736
fb3c807
a36f423
88a5d34
3e978ea
20e7d2c
841fcbb
838f20c
006ec2d
d012fb4
5bc5d64
f9d7eae
37a1bdb
72f2df3
cb6a128
dc818fa
ca94422
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,19 +2,18 @@ | |
// 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 Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.Internal.VisualStudio.PlatformUI; | ||
using Microsoft.VisualStudio.Imaging; | ||
using Microsoft.VisualStudio.Imaging.Interop; | ||
|
||
namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; | ||
|
||
internal partial class AnalyzerItem( | ||
internal sealed partial class AnalyzerItem( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just cleanup. No other changes. |
||
AnalyzersFolderItem analyzersFolder, | ||
AnalyzerReference analyzerReference, | ||
IContextMenuController contextMenuController) : BaseItem(GetNameText(analyzerReference)) | ||
IContextMenuController contextMenuController) | ||
: BaseItem(GetNameText(analyzerReference)) | ||
{ | ||
public AnalyzersFolderItem AnalyzersFolder { get; } = analyzersFolder; | ||
public AnalyzerReference AnalyzerReference { get; } = analyzerReference; | ||
|
@@ -37,14 +36,7 @@ public void Remove() | |
=> this.AnalyzersFolder.RemoveAnalyzer(this.AnalyzerReference.FullPath); | ||
|
||
private static string GetNameText(AnalyzerReference analyzerReference) | ||
{ | ||
if (analyzerReference is UnresolvedAnalyzerReference) | ||
{ | ||
return analyzerReference.FullPath; | ||
} | ||
else | ||
{ | ||
return analyzerReference.Display; | ||
} | ||
} | ||
=> analyzerReference is UnresolvedAnalyzerReference unresolvedAnalyzerReference | ||
? unresolvedAnalyzerReference.FullPath | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this prevents an NRT warning by accessing FullPath through teh derived type. |
||
: analyzerReference.Display; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,237 +2,169 @@ | |
// The .NET Foundation licenses this file to you under the MIT license. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is effectively a rewrite. I recommend SxS diff view. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// See the LICENSE file in the project root for more information. | ||
|
||
#nullable disable | ||
|
||
using System.Collections; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.ComponentModel; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.Diagnostics; | ||
using Microsoft.CodeAnalysis.Internal.Log; | ||
using Microsoft.CodeAnalysis.PooledObjects; | ||
using Microsoft.CodeAnalysis.Remote; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Shared.TestHooks; | ||
using Microsoft.CodeAnalysis.SourceGeneration; | ||
using Microsoft.VisualStudio.Language.Intellisense; | ||
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; | ||
using Microsoft.VisualStudio.Shell; | ||
using Roslyn.Utilities; | ||
|
||
namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer | ||
{ | ||
internal class AnalyzerItemSource : IAttachedCollectionSource, INotifyPropertyChanged | ||
{ | ||
private readonly AnalyzersFolderItem _analyzersFolder; | ||
private readonly IAnalyzersCommandHandler _commandHandler; | ||
private IReadOnlyCollection<AnalyzerReference> _analyzerReferences; | ||
private BulkObservableCollection<AnalyzerItem> _analyzerItems; | ||
|
||
public event PropertyChangedEventHandler PropertyChanged; | ||
|
||
public AnalyzerItemSource(AnalyzersFolderItem analyzersFolder, IAnalyzersCommandHandler commandHandler) | ||
{ | ||
_analyzersFolder = analyzersFolder; | ||
_commandHandler = commandHandler; | ||
|
||
_analyzersFolder.Workspace.WorkspaceChanged += Workspace_WorkspaceChanged; | ||
} | ||
|
||
private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e) | ||
{ | ||
switch (e.Kind) | ||
{ | ||
case WorkspaceChangeKind.SolutionAdded: | ||
case WorkspaceChangeKind.SolutionChanged: | ||
case WorkspaceChangeKind.SolutionReloaded: | ||
UpdateAnalyzers(); | ||
break; | ||
|
||
case WorkspaceChangeKind.SolutionRemoved: | ||
case WorkspaceChangeKind.SolutionCleared: | ||
_analyzersFolder.Workspace.WorkspaceChanged -= Workspace_WorkspaceChanged; | ||
break; | ||
|
||
case WorkspaceChangeKind.ProjectAdded: | ||
case WorkspaceChangeKind.ProjectReloaded: | ||
case WorkspaceChangeKind.ProjectChanged: | ||
if (e.ProjectId == _analyzersFolder.ProjectId) | ||
{ | ||
UpdateAnalyzers(); | ||
} | ||
|
||
break; | ||
|
||
case WorkspaceChangeKind.ProjectRemoved: | ||
if (e.ProjectId == _analyzersFolder.ProjectId) | ||
{ | ||
_analyzersFolder.Workspace.WorkspaceChanged -= Workspace_WorkspaceChanged; | ||
} | ||
|
||
break; | ||
} | ||
} | ||
|
||
private void UpdateAnalyzers() | ||
{ | ||
if (_analyzerItems == null) | ||
{ | ||
// The set of AnalyzerItems hasn't been realized yet. Just signal that HasItems | ||
// may have changed. | ||
namespace Microsoft.VisualStudio.LanguageServices.Implementation.SolutionExplorer; | ||
|
||
NotifyPropertyChanged(nameof(HasItems)); | ||
return; | ||
} | ||
internal sealed class AnalyzerItemSource : IAttachedCollectionSource | ||
{ | ||
private readonly AnalyzersFolderItem _analyzersFolder; | ||
private readonly IAnalyzersCommandHandler _commandHandler; | ||
|
||
var project = _analyzersFolder.Workspace | ||
.CurrentSolution | ||
.GetProject(_analyzersFolder.ProjectId); | ||
private readonly BulkObservableCollection<AnalyzerItem> _items = []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of a collection we overwrite, we now just use a BOC as that can be mutated in place while handling notification to appropriate listeners. |
||
|
||
if (project != null && | ||
project.AnalyzerReferences != _analyzerReferences) | ||
{ | ||
_analyzerReferences = project.AnalyzerReferences; | ||
private readonly CancellationTokenSource _cancellationTokenSource = new(); | ||
private readonly AsyncBatchingWorkQueue _workQueue; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. switched to a work queue model. We listen to workspace vents, which then just pulse the queue. |
||
|
||
_analyzerItems.BeginBulkOperation(); | ||
private IReadOnlyCollection<AnalyzerReference>? _analyzerReferences; | ||
|
||
var itemsToRemove = _analyzerItems | ||
.Where(item => !_analyzerReferences.Contains(item.AnalyzerReference)) | ||
.ToArray(); | ||
private Workspace Workspace => _analyzersFolder.Workspace; | ||
private ProjectId ProjectId => _analyzersFolder.ProjectId; | ||
|
||
var referencesToAdd = GetFilteredAnalyzers(_analyzerReferences, project) | ||
.Where(r => !_analyzerItems.Any(item => item.AnalyzerReference == r)) | ||
.ToArray(); | ||
public AnalyzerItemSource( | ||
AnalyzersFolderItem analyzersFolder, | ||
IAnalyzersCommandHandler commandHandler, | ||
IAsynchronousOperationListenerProvider listenerProvider) | ||
{ | ||
_analyzersFolder = analyzersFolder; | ||
_commandHandler = commandHandler; | ||
|
||
foreach (var item in itemsToRemove) | ||
{ | ||
_analyzerItems.Remove(item); | ||
} | ||
_workQueue = new AsyncBatchingWorkQueue( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically yes. But so minor is unnoticible |
||
DelayTimeSpan.Idle, | ||
ProcessQueueAsync, | ||
listenerProvider.GetListener(FeatureAttribute.SourceGenerators), | ||
_cancellationTokenSource.Token); | ||
|
||
foreach (var reference in referencesToAdd) | ||
{ | ||
_analyzerItems.Add(new AnalyzerItem(_analyzersFolder, reference, _commandHandler.AnalyzerContextMenuController)); | ||
} | ||
this.Workspace.WorkspaceChanged += OnWorkspaceChanged; | ||
_workQueue.AddWork(); | ||
} | ||
|
||
var sorted = _analyzerItems.OrderBy(item => item.AnalyzerReference.Display).ToArray(); | ||
for (var i = 0; i < sorted.Length; i++) | ||
{ | ||
_analyzerItems.Move(_analyzerItems.IndexOf(sorted[i]), i); | ||
} | ||
public object SourceItem => _analyzersFolder; | ||
|
||
_analyzerItems.EndBulkOperation(); | ||
// Defer actual determination and computation of the items until later. | ||
public bool HasItems => !_cancellationTokenSource.IsCancellationRequested; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. correct. This matches what many of our SE nodes do though, which is to guess that they have children while computing asynchronously. |
||
|
||
NotifyPropertyChanged(nameof(HasItems)); | ||
} | ||
} | ||
public IEnumerable Items => _items; | ||
|
||
private void NotifyPropertyChanged(string propertyName) | ||
private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e) | ||
{ | ||
switch (e.Kind) | ||
{ | ||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); | ||
case WorkspaceChangeKind.SolutionAdded: | ||
case WorkspaceChangeKind.SolutionChanged: | ||
case WorkspaceChangeKind.SolutionReloaded: | ||
case WorkspaceChangeKind.SolutionRemoved: | ||
case WorkspaceChangeKind.SolutionCleared: | ||
_workQueue.AddWork(); | ||
break; | ||
|
||
case WorkspaceChangeKind.ProjectAdded: | ||
case WorkspaceChangeKind.ProjectReloaded: | ||
case WorkspaceChangeKind.ProjectChanged: | ||
case WorkspaceChangeKind.ProjectRemoved: | ||
if (e.ProjectId == this.ProjectId) | ||
_workQueue.AddWork(); | ||
|
||
break; | ||
} | ||
} | ||
|
||
public bool HasItems | ||
private async ValueTask ProcessQueueAsync(CancellationToken cancellationToken) | ||
{ | ||
// If the project went away, then shut ourselves down. | ||
var project = this.Workspace.CurrentSolution.GetProject(this.ProjectId); | ||
if (project is null) | ||
{ | ||
get | ||
{ | ||
if (_analyzerItems != null) | ||
{ | ||
return _analyzerItems.Count > 0; | ||
} | ||
|
||
var project = _analyzersFolder.Workspace | ||
.CurrentSolution | ||
.GetProject(_analyzersFolder.ProjectId); | ||
this.Workspace.WorkspaceChanged -= OnWorkspaceChanged; | ||
|
||
if (project != null) | ||
{ | ||
return project.AnalyzerReferences.Count > 0; | ||
} | ||
_cancellationTokenSource.Cancel(); | ||
|
||
return false; | ||
} | ||
// Note: mutating _items will be picked up automatically by clients who are bound to the collection. We do | ||
// not need to notify them through some other mechanism. | ||
_items.Clear(); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return; | ||
} | ||
|
||
public IEnumerable Items | ||
// If nothing changed wrt analyzer references, then there's nothing we need to do. | ||
if (project.AnalyzerReferences == _analyzerReferences) | ||
return; | ||
|
||
// Set the new set of analyzer references we're going to have AnalyzerItems for. | ||
_analyzerReferences = project.AnalyzerReferences; | ||
|
||
var references = await GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( | ||
project, cancellationToken).ConfigureAwait(false); | ||
|
||
await _analyzersFolder.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken); | ||
try | ||
{ | ||
get | ||
{ | ||
if (_analyzerItems == null) | ||
{ | ||
_analyzerItems = []; | ||
|
||
var project = _analyzersFolder.Workspace | ||
.CurrentSolution | ||
.GetProject(_analyzersFolder.ProjectId); | ||
|
||
if (project != null) | ||
{ | ||
_analyzerReferences = project.AnalyzerReferences; | ||
var initialSet = GetFilteredAnalyzers(_analyzerReferences, project) | ||
.OrderBy(ar => ar.Display) | ||
.Select(ar => new AnalyzerItem(_analyzersFolder, ar, _commandHandler.AnalyzerContextMenuController)); | ||
_analyzerItems.AddRange(initialSet); | ||
} | ||
} | ||
_items.BeginBulkOperation(); | ||
|
||
Logger.Log( | ||
FunctionId.SolutionExplorer_AnalyzerItemSource_GetItems, | ||
KeyValueLogMessage.Create(m => m["Count"] = _analyzerItems.Count)); | ||
_items.Clear(); | ||
foreach (var analyzerReference in references.OrderBy(static r => r.Display)) | ||
_items.Add(new AnalyzerItem(_analyzersFolder, analyzerReference, _commandHandler.AnalyzerContextMenuController)); | ||
|
||
return _analyzerItems; | ||
} | ||
return; | ||
} | ||
|
||
public object SourceItem | ||
finally | ||
{ | ||
get | ||
{ | ||
return _analyzersFolder; | ||
} | ||
_items.EndBulkOperation(); | ||
} | ||
|
||
private ImmutableHashSet<string> GetAnalyzersWithLoadErrors() | ||
async Task<ImmutableArray<AnalyzerReference>> GetAnalyzerReferencesWithAnalyzersOrGeneratorsAsync( | ||
Project project, | ||
CancellationToken cancellationToken) | ||
{ | ||
if (_analyzersFolder.Workspace is VisualStudioWorkspaceImpl) | ||
{ | ||
/* | ||
var vsProject = vsWorkspace.DeferredState?.ProjectTracker.GetProject(_analyzersFolder.ProjectId); | ||
var vsAnalyzersMap = vsProject?.GetProjectAnalyzersMap(); | ||
|
||
if (vsAnalyzersMap != null) | ||
{ | ||
return vsAnalyzersMap.Where(kvp => kvp.Value.HasLoadErrors).Select(kvp => kvp.Key).ToImmutableHashSet(); | ||
} | ||
*/ | ||
} | ||
var client = await RemoteHostClient.TryGetClientAsync(this.Workspace, cancellationToken).ConfigureAwait(false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we now call to oop if possible to figure out this information. |
||
|
||
return ImmutableHashSet<string>.Empty; | ||
} | ||
// If we can't make a remote call. Fall back to processing in the VS host. | ||
if (client is null) | ||
return project.AnalyzerReferences.Where(r => r is not AnalyzerFileReference || r.HasAnalyzersOrSourceGenerators(project.Language)).ToImmutableArray(); | ||
|
||
private ImmutableArray<AnalyzerReference> GetFilteredAnalyzers(IEnumerable<AnalyzerReference> analyzerReferences, Project project) | ||
{ | ||
var analyzersWithLoadErrors = GetAnalyzersWithLoadErrors(); | ||
using var connection = client.CreateConnection<IRemoteSourceGenerationService>(callbackTarget: null); | ||
|
||
// Filter out analyzer dependencies which have no diagnostic analyzers, but still retain the unresolved analyzers and analyzers with load errors. | ||
var builder = ArrayBuilder<AnalyzerReference>.GetInstance(); | ||
foreach (var analyzerReference in analyzerReferences) | ||
using var _ = ArrayBuilder<AnalyzerReference>.GetInstance(out var builder); | ||
foreach (var reference in project.AnalyzerReferences) | ||
{ | ||
// Analyzer dependency: | ||
// 1. Must be an Analyzer file reference (we don't understand other analyzer dependencies). | ||
// 2. Mush have no diagnostic analyzers. | ||
// 3. Must have no source generators. | ||
// 4. Must have non-null full path. | ||
// 5. Must not have any assembly or analyzer load failures. | ||
if (analyzerReference is AnalyzerFileReference && | ||
analyzerReference.GetAnalyzers(project.Language).IsDefaultOrEmpty && | ||
analyzerReference.GetGenerators(project.Language).IsDefaultOrEmpty && | ||
analyzerReference.FullPath != null && | ||
!analyzersWithLoadErrors.Contains(analyzerReference.FullPath)) | ||
// Can only remote AnalyzerFileReferences over to the oop side. | ||
if (reference is AnalyzerFileReference analyzerFileReference) | ||
{ | ||
continue; | ||
var result = await connection.TryInvokeAsync<bool>( | ||
project, | ||
(service, solutionChecksum, cancellationToken) => service.HasAnalyzersOrSourceGeneratorsAsync( | ||
solutionChecksum, project.Id, analyzerFileReference.FullPath, cancellationToken), | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
// If the call fails, the OOP substrate will have already reported an error | ||
if (!result.HasValue) | ||
return []; | ||
|
||
if (result.Value) | ||
builder.Add(analyzerFileReference); | ||
} | ||
else if (reference.HasAnalyzersOrSourceGenerators(project.Language)) | ||
{ | ||
builder.Add(reference); | ||
} | ||
|
||
builder.Add(analyzerReference); | ||
} | ||
|
||
return builder.ToImmutableAndFree(); | ||
return builder.ToImmutableAndClear(); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
View with whitespace off.