|
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.Api;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.ExternalAccess.UnitTesting.SolutionCrawler;
internal sealed partial class UnitTestingSolutionCrawlerRegistrationService
{
internal sealed partial class UnitTestingWorkCoordinator : IUnitTestingWorkCoordinator
{
private readonly CountLogAggregator<WorkspaceChangeKind> _logAggregator = new();
private readonly IAsynchronousOperationListener _listener;
private readonly Microsoft.CodeAnalysis.SolutionCrawler.ISolutionCrawlerOptionsService? _solutionCrawlerOptionsService;
private readonly CancellationTokenSource _shutdownNotificationSource = new();
private readonly CancellationToken _shutdownToken;
private readonly AsyncBatchingWorkQueue<Func<Task>> _eventProcessingQueue;
// points to processor task
private readonly UnitTestingIncrementalAnalyzerProcessor _documentAndProjectWorkerProcessor;
private readonly UnitTestingSemanticChangeProcessor _semanticChangeProcessor;
public UnitTestingWorkCoordinator(
IAsynchronousOperationListener listener,
IEnumerable<Lazy<IUnitTestingIncrementalAnalyzerProvider, UnitTestingIncrementalAnalyzerProviderMetadata>> analyzerProviders,
UnitTestingRegistration registration)
{
Registration = registration;
_listener = listener;
_solutionCrawlerOptionsService = Registration.Services.GetService<Microsoft.CodeAnalysis.SolutionCrawler.ISolutionCrawlerOptionsService>();
// event and worker queues
_shutdownToken = _shutdownNotificationSource.Token;
_eventProcessingQueue = new(
TimeSpan.Zero,
ProcessWorkQueueAsync,
listener,
_shutdownToken);
var allFilesWorkerBackOffTimeSpan = UnitTestingSolutionCrawlerTimeSpan.AllFilesWorkerBackOff;
var entireProjectWorkerBackOffTimeSpan = UnitTestingSolutionCrawlerTimeSpan.EntireProjectWorkerBackOff;
_documentAndProjectWorkerProcessor = new UnitTestingIncrementalAnalyzerProcessor(
listener,
analyzerProviders,
Registration,
allFilesWorkerBackOffTimeSpan,
entireProjectWorkerBackOffTimeSpan,
_shutdownToken);
var semanticBackOffTimeSpan = UnitTestingSolutionCrawlerTimeSpan.SemanticChangeBackOff;
var projectBackOffTimeSpan = UnitTestingSolutionCrawlerTimeSpan.ProjectPropagationBackOff;
_semanticChangeProcessor = new UnitTestingSemanticChangeProcessor(listener, Registration, _documentAndProjectWorkerProcessor, semanticBackOffTimeSpan, projectBackOffTimeSpan, _shutdownToken);
}
private async ValueTask ProcessWorkQueueAsync(ImmutableSegmentedList<Func<Task>> list, CancellationToken cancellationToken)
{
foreach (var taskCreator in list)
{
cancellationToken.ThrowIfCancellationRequested();
var task = Task.Run(taskCreator, cancellationToken);
_ = task.ReportNonFatalErrorAsync();
await task.NoThrowAwaitableInternal(captureContext: false);
}
}
public UnitTestingRegistration Registration { get; }
public int CorrelationId => Registration.CorrelationId;
public void AddAnalyzer(IUnitTestingIncrementalAnalyzer analyzer)
{
// add analyzer
_documentAndProjectWorkerProcessor.AddAnalyzer(analyzer);
// and ask to re-analyze whole solution for the given analyzer
var scope = new UnitTestingReanalyzeScope(Registration.GetSolutionToAnalyze().Id);
Reanalyze(analyzer, scope);
}
public void Reanalyze(IUnitTestingIncrementalAnalyzer analyzer, UnitTestingReanalyzeScope scope)
{
_eventProcessingQueue.AddWork(() => EnqueueWorkItemAsync(analyzer, scope));
if (scope.HasMultipleDocuments)
{
// log big reanalysis request from things like fix all, suppress all or option changes
// we are not interested in 1 file re-analysis request which can happen from like venus typing
var solution = Registration.GetSolutionToAnalyze();
UnitTestingSolutionCrawlerLogger.LogReanalyze(
CorrelationId, analyzer, scope.GetDocumentCount(solution), scope.GetLanguagesStringForTelemetry(solution));
}
}
public void OnWorkspaceChanged(WorkspaceChangeEventArgs args)
{
// guard us from cancellation
try
{
ProcessEvent(args);
}
catch (OperationCanceledException oce)
{
if (NotOurShutdownToken(oce))
{
throw;
}
// it is our cancellation, ignore
}
catch (AggregateException ae)
{
ae = ae.Flatten();
// If we had a mix of exceptions, don't eat it
if (ae.InnerExceptions.Any(e => e is not OperationCanceledException) ||
ae.InnerExceptions.Cast<OperationCanceledException>().Any(NotOurShutdownToken))
{
// We had a cancellation with a different token, so don't eat it
throw;
}
// it is our cancellation, ignore
}
}
private bool NotOurShutdownToken(OperationCanceledException oce)
=> oce.CancellationToken == _shutdownToken;
private void ProcessEvent(WorkspaceChangeEventArgs args)
{
UnitTestingSolutionCrawlerLogger.LogWorkspaceEvent(_logAggregator, args.Kind);
// TODO: add telemetry that record how much it takes to process an event (max, min, average and etc)
switch (args.Kind)
{
case WorkspaceChangeKind.SolutionAdded:
EnqueueFullSolutionEvent(args.NewSolution, UnitTestingInvocationReasons.DocumentAdded);
break;
case WorkspaceChangeKind.SolutionChanged:
case WorkspaceChangeKind.SolutionReloaded:
EnqueueSolutionChangedEvent(args.OldSolution, args.NewSolution);
break;
case WorkspaceChangeKind.SolutionCleared:
case WorkspaceChangeKind.SolutionRemoved:
// Not used in unit testing crawling
break;
case WorkspaceChangeKind.ProjectAdded:
Contract.ThrowIfNull(args.ProjectId);
EnqueueFullProjectEvent(args.NewSolution, args.ProjectId, UnitTestingInvocationReasons.DocumentAdded);
break;
case WorkspaceChangeKind.ProjectChanged:
case WorkspaceChangeKind.ProjectReloaded:
Contract.ThrowIfNull(args.ProjectId);
EnqueueProjectChangedEvent(args.OldSolution, args.NewSolution, args.ProjectId);
break;
case WorkspaceChangeKind.ProjectRemoved:
Contract.ThrowIfNull(args.ProjectId);
EnqueueFullProjectEvent(args.OldSolution, args.ProjectId, UnitTestingInvocationReasons.DocumentRemoved);
break;
case WorkspaceChangeKind.DocumentAdded:
Contract.ThrowIfNull(args.DocumentId);
EnqueueFullDocumentEvent(args.NewSolution, args.DocumentId, UnitTestingInvocationReasons.DocumentAdded);
break;
case WorkspaceChangeKind.DocumentReloaded:
case WorkspaceChangeKind.DocumentChanged:
Contract.ThrowIfNull(args.DocumentId);
EnqueueDocumentChangedEvent(args.OldSolution, args.NewSolution, args.DocumentId);
break;
case WorkspaceChangeKind.DocumentRemoved:
Contract.ThrowIfNull(args.DocumentId);
EnqueueFullDocumentEvent(args.OldSolution, args.DocumentId, UnitTestingInvocationReasons.DocumentRemoved);
break;
case WorkspaceChangeKind.AdditionalDocumentAdded:
case WorkspaceChangeKind.AdditionalDocumentRemoved:
case WorkspaceChangeKind.AdditionalDocumentChanged:
case WorkspaceChangeKind.AdditionalDocumentReloaded:
case WorkspaceChangeKind.AnalyzerConfigDocumentAdded:
case WorkspaceChangeKind.AnalyzerConfigDocumentRemoved:
case WorkspaceChangeKind.AnalyzerConfigDocumentChanged:
case WorkspaceChangeKind.AnalyzerConfigDocumentReloaded:
// If an additional file or .editorconfig has changed we need to reanalyze the entire project.
Contract.ThrowIfNull(args.ProjectId);
EnqueueFullProjectEvent(args.NewSolution, args.ProjectId, UnitTestingInvocationReasons.AdditionalDocumentChanged);
break;
default:
throw ExceptionUtilities.UnexpectedValue(args.Kind);
}
}
private void EnqueueSolutionChangedEvent(Solution oldSolution, Solution newSolution)
{
_eventProcessingQueue.AddWork(
async () =>
{
var solutionChanges = newSolution.GetChanges(oldSolution);
// TODO: Async version for GetXXX methods?
foreach (var addedProject in solutionChanges.GetAddedProjects())
{
await EnqueueFullProjectWorkItemAsync(addedProject, UnitTestingInvocationReasons.DocumentAdded).ConfigureAwait(false);
}
foreach (var projectChanges in solutionChanges.GetProjectChanges())
{
await EnqueueWorkItemAsync(projectChanges).ConfigureAwait(continueOnCapturedContext: false);
}
foreach (var removedProject in solutionChanges.GetRemovedProjects())
{
await EnqueueFullProjectWorkItemAsync(removedProject, UnitTestingInvocationReasons.DocumentRemoved).ConfigureAwait(false);
}
});
}
private void EnqueueFullSolutionEvent(Solution solution, UnitTestingInvocationReasons invocationReasons)
{
_eventProcessingQueue.AddWork(
async () =>
{
foreach (var projectId in solution.ProjectIds)
{
await EnqueueFullProjectWorkItemAsync(solution.GetRequiredProject(projectId), invocationReasons).ConfigureAwait(false);
}
});
}
private void EnqueueProjectChangedEvent(Solution oldSolution, Solution newSolution, ProjectId projectId)
{
_eventProcessingQueue.AddWork(
async () =>
{
var oldProject = oldSolution.GetRequiredProject(projectId);
var newProject = newSolution.GetRequiredProject(projectId);
await EnqueueWorkItemAsync(newProject.GetChanges(oldProject)).ConfigureAwait(false);
});
}
private void EnqueueFullProjectEvent(Solution solution, ProjectId projectId, UnitTestingInvocationReasons invocationReasons)
{
_eventProcessingQueue.AddWork(
() => EnqueueFullProjectWorkItemAsync(solution.GetRequiredProject(projectId), invocationReasons));
}
private void EnqueueFullDocumentEvent(Solution solution, DocumentId documentId, UnitTestingInvocationReasons invocationReasons)
{
_eventProcessingQueue.AddWork(
() =>
{
var project = solution.GetRequiredProject(documentId.ProjectId);
return EnqueueDocumentWorkItemAsync(project, documentId, document: null, invocationReasons);
});
}
private void EnqueueDocumentChangedEvent(Solution oldSolution, Solution newSolution, DocumentId documentId)
{
// document changed event is the special one.
_eventProcessingQueue.AddWork(
async () =>
{
var oldProject = oldSolution.GetRequiredProject(documentId.ProjectId);
var newProject = newSolution.GetRequiredProject(documentId.ProjectId);
await EnqueueChangedDocumentWorkItemAsync(oldProject.GetRequiredDocument(documentId), newProject.GetRequiredDocument(documentId)).ConfigureAwait(false);
// If all features are enabled for source generated documents, the solution crawler needs to
// include them in incremental analysis.
if (_solutionCrawlerOptionsService?.EnableDiagnosticsInSourceGeneratedFiles == true)
{
// TODO: if this becomes a hot spot, we should be able to expose/access the dictionary
// underneath GetSourceGeneratedDocumentsAsync rather than create a new one here.
var oldProjectSourceGeneratedDocuments = await oldProject.GetSourceGeneratedDocumentsAsync(_shutdownToken).ConfigureAwait(false);
var oldProjectSourceGeneratedDocumentsById = oldProjectSourceGeneratedDocuments.ToDictionary(static document => document.Id);
var newProjectSourceGeneratedDocuments = await newProject.GetSourceGeneratedDocumentsAsync(_shutdownToken).ConfigureAwait(false);
var newProjectSourceGeneratedDocumentsById = newProjectSourceGeneratedDocuments.ToDictionary(static document => document.Id);
foreach (var (oldDocumentId, _) in oldProjectSourceGeneratedDocumentsById)
{
if (!newProjectSourceGeneratedDocumentsById.ContainsKey(oldDocumentId))
{
// This source generated document was removed
EnqueueFullDocumentEvent(oldSolution, oldDocumentId, UnitTestingInvocationReasons.DocumentRemoved);
}
}
foreach (var (newDocumentId, newDocument) in newProjectSourceGeneratedDocumentsById)
{
if (!oldProjectSourceGeneratedDocumentsById.TryGetValue(newDocumentId, out var oldDocument))
{
// This source generated document was added
EnqueueFullDocumentEvent(newSolution, newDocumentId, UnitTestingInvocationReasons.DocumentAdded);
}
else
{
// This source generated document may have changed
await EnqueueChangedDocumentWorkItemAsync(oldDocument, newDocument).ConfigureAwait(continueOnCapturedContext: false);
}
}
}
});
}
private async Task EnqueueDocumentWorkItemAsync(Project project, DocumentId documentId, TextDocument? document, UnitTestingInvocationReasons invocationReasons, SyntaxNode? changedMember = null)
{
// we are shutting down
_shutdownToken.ThrowIfCancellationRequested();
var priorityService = project.GetLanguageService<IUnitTestingWorkCoordinatorPriorityService>();
document ??= project.GetTextDocument(documentId);
var sourceDocument = document as Document;
var isLowPriority = priorityService != null && sourceDocument != null && await priorityService.IsLowPriorityAsync(sourceDocument, _shutdownToken).ConfigureAwait(false);
var currentMember = GetSyntaxPath(changedMember);
// call to this method is serialized. and only this method does the writing.
_documentAndProjectWorkerProcessor.Enqueue(
new UnitTestingWorkItem(documentId, project.Language, invocationReasons, isLowPriority, currentMember, _listener.BeginAsyncOperation("WorkItem")));
// enqueue semantic work planner
if (invocationReasons.Contains(UnitTestingPredefinedInvocationReasons.SemanticChanged) && sourceDocument != null)
{
// must use "Document" here so that the snapshot doesn't go away. we need the snapshot to calculate p2p dependency graph later.
// due to this, we might hold onto solution (and things kept alive by it) little bit longer than usual.
_semanticChangeProcessor.Enqueue(project, documentId, sourceDocument, currentMember);
}
}
private static Document GetRequiredDocument(Project project, DocumentId documentId, Document? document)
=> document ?? project.GetRequiredDocument(documentId);
private static SyntaxPath? GetSyntaxPath(SyntaxNode? changedMember)
{
// using syntax path might be too expansive since it will be created on every keystroke.
// but currently, we have no other way to track a node between two different tree (even for incrementally parsed one)
if (changedMember == null)
{
return null;
}
return new SyntaxPath(changedMember);
}
private async Task EnqueueFullProjectWorkItemAsync(Project project, UnitTestingInvocationReasons invocationReasons)
{
foreach (var documentId in project.DocumentIds)
await EnqueueDocumentWorkItemAsync(project, documentId, document: null, invocationReasons).ConfigureAwait(false);
foreach (var documentId in project.AdditionalDocumentIds)
await EnqueueDocumentWorkItemAsync(project, documentId, document: null, invocationReasons).ConfigureAwait(false);
foreach (var documentId in project.AnalyzerConfigDocumentIds)
await EnqueueDocumentWorkItemAsync(project, documentId, document: null, invocationReasons).ConfigureAwait(false);
// If all features are enabled for source generated documents, the solution crawler needs to
// include them in incremental analysis.
if (_solutionCrawlerOptionsService?.EnableDiagnosticsInSourceGeneratedFiles == true)
{
foreach (var document in await project.GetSourceGeneratedDocumentsAsync(_shutdownToken).ConfigureAwait(false))
await EnqueueDocumentWorkItemAsync(project, document.Id, document, invocationReasons).ConfigureAwait(false);
}
}
private async Task EnqueueWorkItemAsync(IUnitTestingIncrementalAnalyzer analyzer, UnitTestingReanalyzeScope scope)
{
var solution = Registration.GetSolutionToAnalyze();
var invocationReasons =
UnitTestingInvocationReasons.Reanalyze;
foreach (var (project, documentId) in scope.GetDocumentIds(solution))
await EnqueueWorkItemAsync(analyzer, project, documentId, document: null, invocationReasons).ConfigureAwait(false);
}
private async Task EnqueueWorkItemAsync(
IUnitTestingIncrementalAnalyzer analyzer, Project project, DocumentId documentId, Document? document, UnitTestingInvocationReasons invocationReasons)
{
var priorityService = project.GetLanguageService<IUnitTestingWorkCoordinatorPriorityService>();
var isLowPriority = priorityService != null && await priorityService.IsLowPriorityAsync(
GetRequiredDocument(project, documentId, document), _shutdownToken).ConfigureAwait(false);
_documentAndProjectWorkerProcessor.Enqueue(
new UnitTestingWorkItem(documentId, project.Language, invocationReasons,
isLowPriority, analyzer, _listener.BeginAsyncOperation("WorkItem")));
}
private async Task EnqueueWorkItemAsync(ProjectChanges projectChanges)
{
await EnqueueProjectConfigurationChangeWorkItemAsync(projectChanges).ConfigureAwait(false);
foreach (var addedDocumentId in projectChanges.GetAddedDocuments())
await EnqueueDocumentWorkItemAsync(projectChanges.NewProject, addedDocumentId, document: null, UnitTestingInvocationReasons.DocumentAdded).ConfigureAwait(false);
foreach (var changedDocumentId in projectChanges.GetChangedDocuments())
{
await EnqueueChangedDocumentWorkItemAsync(projectChanges.OldProject.GetRequiredDocument(changedDocumentId), projectChanges.NewProject.GetRequiredDocument(changedDocumentId))
.ConfigureAwait(continueOnCapturedContext: false);
}
foreach (var removedDocumentId in projectChanges.GetRemovedDocuments())
await EnqueueDocumentWorkItemAsync(projectChanges.OldProject, removedDocumentId, document: null, UnitTestingInvocationReasons.DocumentRemoved).ConfigureAwait(false);
}
private async Task EnqueueProjectConfigurationChangeWorkItemAsync(ProjectChanges projectChanges)
{
var oldProject = projectChanges.OldProject;
var newProject = projectChanges.NewProject;
// TODO: why solution changes return Project not ProjectId but ProjectChanges return DocumentId not Document?
var projectConfigurationChange = UnitTestingInvocationReasons.Empty;
if (projectChanges.GetAddedMetadataReferences().Any() ||
projectChanges.GetAddedProjectReferences().Any() ||
projectChanges.GetAddedAnalyzerReferences().Any() ||
projectChanges.GetRemovedMetadataReferences().Any() ||
projectChanges.GetRemovedProjectReferences().Any() ||
projectChanges.GetRemovedAnalyzerReferences().Any() ||
!object.Equals(oldProject.CompilationOptions, newProject.CompilationOptions) ||
!object.Equals(oldProject.AssemblyName, newProject.AssemblyName) ||
!object.Equals(oldProject.Name, newProject.Name) ||
!object.Equals(oldProject.AnalyzerOptions, newProject.AnalyzerOptions) ||
!object.Equals(oldProject.HostAnalyzerOptions, newProject.HostAnalyzerOptions) ||
!object.Equals(oldProject.DefaultNamespace, newProject.DefaultNamespace) ||
!object.Equals(oldProject.OutputFilePath, newProject.OutputFilePath) ||
!object.Equals(oldProject.OutputRefFilePath, newProject.OutputRefFilePath) ||
!oldProject.CompilationOutputInfo.Equals(newProject.CompilationOutputInfo) ||
oldProject.State.RunAnalyzers != newProject.State.RunAnalyzers)
{
projectConfigurationChange = projectConfigurationChange.With(UnitTestingInvocationReasons.ProjectConfigurationChanged);
}
if (!projectConfigurationChange.IsEmpty)
{
await EnqueueFullProjectWorkItemAsync(projectChanges.NewProject, projectConfigurationChange).ConfigureAwait(false);
}
}
private async Task EnqueueChangedDocumentWorkItemAsync(Document oldDocument, Document newDocument)
{
var differenceService = newDocument.GetLanguageService<IUnitTestingDocumentDifferenceService>();
if (differenceService == null)
{
// For languages that don't use a Roslyn syntax tree, they don't export a document difference service.
// The whole document should be considered as changed in that case.
await EnqueueDocumentWorkItemAsync(newDocument.Project, newDocument.Id, newDocument, UnitTestingInvocationReasons.DocumentChanged).ConfigureAwait(false);
}
else
{
var differenceResult = differenceService.GetDifference(oldDocument, newDocument, _shutdownToken);
if (differenceResult != null)
await EnqueueDocumentWorkItemAsync(newDocument.Project, newDocument.Id, newDocument, differenceResult.ChangeType, differenceResult.ChangedMember).ConfigureAwait(false);
}
}
internal TestAccessor GetTestAccessor()
{
return new TestAccessor(this);
}
internal readonly struct TestAccessor
{
private readonly UnitTestingWorkCoordinator _workCoordinator;
internal TestAccessor(UnitTestingWorkCoordinator workCoordinator)
{
_workCoordinator = workCoordinator;
}
internal void WaitUntilCompletion(ImmutableArray<IUnitTestingIncrementalAnalyzer> workers)
{
var solution = _workCoordinator.Registration.GetSolutionToAnalyze();
var list = new List<UnitTestingWorkItem>();
foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
list.Add(new UnitTestingWorkItem(document.Id, document.Project.Language, UnitTestingInvocationReasons.DocumentAdded, isLowPriority: false, activeMember: null, EmptyAsyncToken.Instance));
}
}
_workCoordinator._documentAndProjectWorkerProcessor.GetTestAccessor().WaitUntilCompletion(workers, list);
}
internal void WaitUntilCompletion()
=> _workCoordinator._documentAndProjectWorkerProcessor.GetTestAccessor().WaitUntilCompletion();
}
}
internal readonly struct UnitTestingReanalyzeScope
{
private readonly SolutionId? _solutionId;
private readonly ISet<object>? _projectOrDocumentIds;
public UnitTestingReanalyzeScope(SolutionId solutionId)
{
_solutionId = solutionId;
_projectOrDocumentIds = null;
}
public UnitTestingReanalyzeScope(IEnumerable<ProjectId>? projectIds = null, IEnumerable<DocumentId>? documentIds = null)
{
projectIds ??= [];
documentIds ??= [];
_solutionId = null;
_projectOrDocumentIds = new HashSet<object>(projectIds);
foreach (var documentId in documentIds)
{
if (_projectOrDocumentIds.Contains(documentId.ProjectId))
{
continue;
}
_projectOrDocumentIds.Add(documentId);
}
}
public bool HasMultipleDocuments => _solutionId != null || _projectOrDocumentIds?.Count > 1;
public string GetLanguagesStringForTelemetry(Solution solution)
{
if (_solutionId != null && solution.Id != _solutionId)
{
// return empty if given solution is not
// same as solution this scope is created for
return string.Empty;
}
using var pool = SharedPools.Default<HashSet<string>>().GetPooledObject();
if (_solutionId != null)
{
pool.Object.UnionWith(solution.SolutionState.ProjectStates.Select(kv => kv.Value.Language));
return string.Join(",", pool.Object);
}
Contract.ThrowIfNull(_projectOrDocumentIds);
foreach (var projectOrDocumentId in _projectOrDocumentIds)
{
switch (projectOrDocumentId)
{
case ProjectId projectId:
var project = solution.GetProject(projectId);
if (project != null)
{
pool.Object.Add(project.Language);
}
break;
case DocumentId documentId:
var document = solution.GetDocument(documentId);
if (document != null)
{
pool.Object.Add(document.Project.Language);
}
break;
default:
throw ExceptionUtilities.UnexpectedValue(projectOrDocumentId);
}
}
return string.Join(",", pool.Object);
}
public int GetDocumentCount(Solution solution)
{
if (_solutionId != null && solution.Id != _solutionId)
{
return 0;
}
var count = 0;
if (_solutionId != null)
{
foreach (var projectState in solution.SolutionState.ProjectStates)
{
count += projectState.Value.DocumentStates.Count;
}
return count;
}
Contract.ThrowIfNull(_projectOrDocumentIds);
foreach (var projectOrDocumentId in _projectOrDocumentIds)
{
switch (projectOrDocumentId)
{
case ProjectId projectId:
var project = solution.GetProject(projectId);
if (project != null)
{
count += project.DocumentIds.Count;
}
break;
case DocumentId documentId:
count++;
break;
default:
throw ExceptionUtilities.UnexpectedValue(projectOrDocumentId);
}
}
return count;
}
public IEnumerable<(Project project, DocumentId documentId)> GetDocumentIds(Solution solution)
{
if (_solutionId != null && solution.Id != _solutionId)
{
yield break;
}
if (_solutionId != null)
{
foreach (var project in solution.Projects)
{
foreach (var documentId in project.DocumentIds)
yield return (project, documentId);
}
yield break;
}
Contract.ThrowIfNull(_projectOrDocumentIds);
foreach (var projectOrDocumentId in _projectOrDocumentIds)
{
switch (projectOrDocumentId)
{
case ProjectId projectId:
{
var project = solution.GetProject(projectId);
if (project != null)
{
foreach (var documentId in project.DocumentIds)
yield return (project, documentId);
}
break;
}
case DocumentId documentId:
{
var project = solution.GetProject(documentId.ProjectId);
if (project != null)
{
// ReanalyzeScopes are created and held in a queue before they are processed later; it's possible the document
// that we queued for is no longer present.
if (project.ContainsDocument(documentId))
yield return (project, documentId);
}
break;
}
}
}
}
}
}
|