diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/ISolutionExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/ISolutionExtensions.cs index c5034aad4bccb..b917eb25f2338 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/ISolutionExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/ISolutionExtensions.cs @@ -59,10 +59,5 @@ public static Solution WithTextDocumentText(this Solution solution, DocumentId d throw ExceptionUtilities.UnexpectedValue(documentKind); } } - - public static ImmutableArray FilterDocumentIdsByLanguage(this Solution solution, ImmutableArray documentIds, string language) - => documentIds.WhereAsArray( - (documentId, args) => args.solution.GetDocument(documentId)?.Project.Language == args.language, - (solution, language)); } } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs index 61b709e5ccce6..b7641f11507b5 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Document.cs @@ -430,8 +430,7 @@ public async Task> GetTextChangesAsync(Document oldDocum /// public ImmutableArray GetLinkedDocumentIds() { - var documentIdsWithPath = this.Project.Solution.GetDocumentIdsWithFilePath(this.FilePath); - var filteredDocumentIds = this.Project.Solution.FilterDocumentIdsByLanguage(documentIdsWithPath, this.Project.Language); + var filteredDocumentIds = this.Project.Solution.GetRelatedDocumentIds(this.Id); return filteredDocumentIds.Remove(this.Id); } diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs index ecfea4241e501..088c8d2906356 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/Solution.cs @@ -1688,29 +1688,7 @@ internal async Task WithMergedLinkedFileChangesAsync( internal ImmutableArray GetRelatedDocumentIds(DocumentId documentId) { - var projectState = _state.GetProjectState(documentId.ProjectId); - if (projectState == null) - { - // this document no longer exist - return ImmutableArray.Empty; - } - - var documentState = projectState.DocumentStates.GetState(documentId); - if (documentState == null) - { - // this document no longer exist - return ImmutableArray.Empty; - } - - var filePath = documentState.FilePath; - if (string.IsNullOrEmpty(filePath)) - { - // this document can't have any related document. only related document is itself. - return ImmutableArray.Create(documentId); - } - - var documentIds = GetDocumentIdsWithFilePath(filePath); - return this.FilterDocumentIdsByLanguage(documentIds, projectState.ProjectInfo.Language); + return _state.GetRelatedDocumentIds(documentId); } internal Solution WithNewWorkspace(Workspace workspace, int workspaceVersion) diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs index d891fd758a071..bf7e0f505110e 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.cs @@ -1627,8 +1627,14 @@ public SolutionState WithFrozenPartialCompilationIncludingSpecificDocument(Docum { try { - var doc = this.GetRequiredDocumentState(documentId); - var tree = doc.GetSyntaxTree(cancellationToken); + var allDocumentIds = GetRelatedDocumentIds(documentId); + using var _ = ArrayBuilder<(DocumentState, SyntaxTree)>.GetInstance(allDocumentIds.Length, out var builder); + + foreach (var currentDocumentId in allDocumentIds) + { + var document = this.GetRequiredDocumentState(currentDocumentId); + builder.Add((document, document.GetSyntaxTree(cancellationToken))); + } using (this.StateLock.DisposableWait(cancellationToken)) { @@ -1652,13 +1658,19 @@ public SolutionState WithFrozenPartialCompilationIncludingSpecificDocument(Docum return currentPartialSolution!; } - // if we don't have one or it is stale, create a new partial solution - var tracker = this.GetCompilationTracker(documentId.ProjectId); - var newTracker = tracker.FreezePartialStateWithTree(this, doc, tree, cancellationToken); + var newIdToProjectStateMap = _projectIdToProjectStateMap; + var newIdToTrackerMap = _projectIdToTrackerMap; - Contract.ThrowIfFalse(_projectIdToProjectStateMap.ContainsKey(documentId.ProjectId)); - var newIdToProjectStateMap = _projectIdToProjectStateMap.SetItem(documentId.ProjectId, newTracker.ProjectState); - var newIdToTrackerMap = _projectIdToTrackerMap.SetItem(documentId.ProjectId, newTracker); + foreach (var (doc, tree) in builder) + { + // if we don't have one or it is stale, create a new partial solution + var tracker = this.GetCompilationTracker(doc.Id.ProjectId); + var newTracker = tracker.FreezePartialStateWithTree(this, doc, tree, cancellationToken); + + Contract.ThrowIfFalse(newIdToProjectStateMap.ContainsKey(doc.Id.ProjectId)); + newIdToProjectStateMap = newIdToProjectStateMap.SetItem(doc.Id.ProjectId, newTracker.ProjectState); + newIdToTrackerMap = newIdToTrackerMap.SetItem(doc.Id.ProjectId, newTracker); + } currentPartialSolution = this.Branch( idToProjectStateMap: newIdToProjectStateMap, @@ -1679,6 +1691,48 @@ public SolutionState WithFrozenPartialCompilationIncludingSpecificDocument(Docum } } + public ImmutableArray GetRelatedDocumentIds(DocumentId documentId) + { + var projectState = this.GetProjectState(documentId.ProjectId); + if (projectState == null) + { + // this document no longer exist + return ImmutableArray.Empty; + } + + var documentState = projectState.DocumentStates.GetState(documentId); + if (documentState == null) + { + // this document no longer exist + return ImmutableArray.Empty; + } + + var filePath = documentState.FilePath; + if (string.IsNullOrEmpty(filePath)) + { + // this document can't have any related document. only related document is itself. + return ImmutableArray.Create(documentId); + } + + var documentIds = GetDocumentIdsWithFilePath(filePath); + return FilterDocumentIdsByLanguage(this, documentIds, projectState.ProjectInfo.Language); + } + + private static ImmutableArray FilterDocumentIdsByLanguage(SolutionState solution, ImmutableArray documentIds, string language) + => documentIds.WhereAsArray( + static (documentId, args) => + { + var projectState = args.solution.GetProjectState(documentId.ProjectId); + if (projectState == null) + { + // this document no longer exist + return false; + } + + return projectState.ProjectInfo.Language == args.language; + }, + (solution, language)); + /// /// Creates a new solution instance with all the documents specified updated to have the same specified text. /// diff --git a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs index cceca9efe2ed7..eda771ebafcd7 100644 --- a/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs +++ b/src/Workspaces/CoreTest/SolutionTests/SolutionWithSourceGeneratorTests.cs @@ -752,6 +752,37 @@ public async Task ForkAfterFreezeNoLongerRunsGenerators() Assert.Equal("// Something else", (await document.GetRequiredSyntaxRootAsync(CancellationToken.None)).ToFullString()); } + [Fact] + public async Task LinkedDocumentOfFrozenShouldNotRunSourceGenerator() + { + using var workspace = CreateWorkspaceWithPartialSemantics(); + var generatorRan = false; + var analyzerReference = new TestGeneratorReference(new CallbackGenerator(_ => { }, onExecute: _ => { generatorRan = true; }, source: "// Hello World!")); + + var originalDocument1 = AddEmptyProject(workspace.CurrentSolution, name: "Project1") + .AddAnalyzerReference(analyzerReference) + .AddDocument("RegularDocument.cs", "// Source File", filePath: "RegularDocument.cs"); + + // this is a linked document of document1 above + var originalDocument2 = AddEmptyProject(originalDocument1.Project.Solution, name: "Project2") + .AddAnalyzerReference(analyzerReference) + .AddDocument(originalDocument1.Name, await originalDocument1.GetTextAsync().ConfigureAwait(false), filePath: originalDocument1.FilePath); + + var frozenSolution = originalDocument2.WithFrozenPartialSemantics(CancellationToken.None).Project.Solution; + var documentIdsToTest = new[] { originalDocument1.Id, originalDocument2.Id }; + + foreach (var documentIdToTest in documentIdsToTest) + { + var document = frozenSolution.GetRequiredDocument(documentIdToTest); + Assert.Equal(document.GetLinkedDocumentIds().Single(), documentIdsToTest.Except(new[] { documentIdToTest }).Single()); + document = document.WithText(SourceText.From("// Something else")); + + var compilation = await document.Project.GetRequiredCompilationAsync(CancellationToken.None); + Assert.Single(compilation.SyntaxTrees); + Assert.False(generatorRan); + } + } + [Fact] public async Task DynamicFilesNotPassedToSourceGenerators() {