Skip to content
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

Merged
merged 19 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@
// The .NET Foundation licenses this file to you under the MIT license.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View with whitespace off.

// 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(
Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Expand All @@ -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
Copy link
Member Author

Choose a reason for hiding this comment

The 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
Expand Up @@ -2,237 +2,169 @@
// The .NET Foundation licenses this file to you under the MIT license.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is effectively a rewrite. I recommend SxS diff view.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was rewritten in the style of #74444 and #74448

// 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 = [];
Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Copy link
Member Author

Choose a reason for hiding this comment

The 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AsyncBatchingWorkQueue

ABWQ to the rescue again!!! Will there be a user-facing change here due to the delay between the Workspace event and the collection update?

Copy link
Member Author

Choose a reason for hiding this comment

The 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasItems

Is this used in solution explorer to determine if it is expandable?

Copy link
Member Author

Choose a reason for hiding this comment

The 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);
Copy link
Member Author

Choose a reason for hiding this comment

The 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();
}
}
}
Loading