Skip to content

Commit

Permalink
Merge pull request dotnet#38064 from jasonmalinowski/fix-metadata-as-…
Browse files Browse the repository at this point in the history
…source-caching

Fix Metadata as Source caching stale generated files
  • Loading branch information
jasonmalinowski authored Aug 19, 2019
2 parents 89d305d + a86cc66 commit 5551fad
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
Expand Down Expand Up @@ -36,14 +38,14 @@ internal class MetadataAsSourceFileService : IMetadataAsSourceFileService
private readonly Dictionary<string, MetadataAsSourceGeneratedFileInfo> _generatedFilenameToInformation = new Dictionary<string, MetadataAsSourceGeneratedFileInfo>(StringComparer.OrdinalIgnoreCase);
private IBidirectionalMap<MetadataAsSourceGeneratedFileInfo, DocumentId> _openedDocumentIds = BidirectionalMap<MetadataAsSourceGeneratedFileInfo, DocumentId>.Empty;

private MetadataAsSourceWorkspace _workspace;
private MetadataAsSourceWorkspace? _workspace;

/// <summary>
/// We create a mutex so other processes can see if our directory is still alive. We destroy the mutex when
/// we purge our generated files.
/// </summary>
private Mutex _mutex;
private string _rootTemporaryPathWithGuid;
private Mutex? _mutex;
private string? _rootTemporaryPathWithGuid;
private readonly string _rootTemporaryPath;

[ImportingConstructor]
Expand Down Expand Up @@ -89,14 +91,15 @@ public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(Project project, I
symbol = symbol.GetOriginalUnreducedDefinition();

MetadataAsSourceGeneratedFileInfo fileInfo;
Location navigateLocation = null;
Location? navigateLocation = null;
var topLevelNamedType = MetadataAsSourceHelpers.GetTopLevelContainingNamedType(symbol);
var symbolId = SymbolKey.Create(symbol, cancellationToken);
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);

using (await _gate.DisposableWaitAsync(cancellationToken).ConfigureAwait(false))
{
InitializeWorkspace(project);
Contract.ThrowIfNull(_workspace);

var infoKey = await GetUniqueDocumentKey(project, topLevelNamedType, allowDecompilation, cancellationToken).ConfigureAwait(false);
fileInfo = _keyToInformation.GetOrAdd(infoKey, _ => new MetadataAsSourceGeneratedFileInfo(GetRootPathWithGuid_NoLock(), project, topLevelNamedType, allowDecompilation));
Expand All @@ -111,6 +114,8 @@ public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(Project project, I
var temporaryDocument = _workspace.CurrentSolution.AddProject(temporaryProjectInfoAndDocumentId.Item1)
.GetDocument(temporaryProjectInfoAndDocumentId.Item2);

Contract.ThrowIfNull(temporaryDocument, "The temporary ProjectInfo didn't contain the document it said it would.");

var useDecompiler = allowDecompilation;
if (useDecompiler)
{
Expand Down Expand Up @@ -140,7 +145,7 @@ public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(Project project, I

if (!useDecompiler)
{
var sourceFromMetadataService = temporaryDocument.Project.LanguageServices.GetService<IMetadataAsSourceService>();
var sourceFromMetadataService = temporaryDocument.Project.LanguageServices.GetRequiredService<IMetadataAsSourceService>();
temporaryDocument = await sourceFromMetadataService.AddSourceToAsync(temporaryDocument, compilation, symbol, cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -195,6 +200,8 @@ public async Task<MetadataAsSourceFile> GetGeneratedFileAsync(Project project, I

private async Task<Location> RelocateSymbol_NoLock(MetadataAsSourceGeneratedFileInfo fileInfo, SymbolKey symbolId, CancellationToken cancellationToken)
{
Contract.ThrowIfNull(_workspace);

// We need to relocate the symbol in the already existing file. If the file is open, we can just
// reuse that workspace. Otherwise, we have to go spin up a temporary project to do the binding.
if (_openedDocumentIds.TryGetValue(fileInfo, out var openDocumentId))
Expand All @@ -217,6 +224,8 @@ public bool TryAddDocumentToWorkspace(string filePath, ITextBuffer buffer)
{
using (_gate.DisposableWait())
{
Contract.ThrowIfNull(_workspace);

if (_generatedFilenameToInformation.TryGetValue(filePath, out var fileInfo))
{
Contract.ThrowIfTrue(_openedDocumentIds.ContainsKey(fileInfo));
Expand Down Expand Up @@ -258,6 +267,7 @@ private void RemoveDocumentFromWorkspace_NoLock(MetadataAsSourceGeneratedFileInf
{
var documentId = _openedDocumentIds.GetValueOrDefault(fileInfo);
Contract.ThrowIfNull(documentId);
Contract.ThrowIfNull(_workspace);

_workspace.OnDocumentClosed(documentId, new FileTextLoader(fileInfo.TemporaryFilePath, fileInfo.Encoding));
_workspace.OnProjectRemoved(documentId.ProjectId);
Expand All @@ -268,15 +278,18 @@ private void RemoveDocumentFromWorkspace_NoLock(MetadataAsSourceGeneratedFileInf
private async Task<UniqueDocumentKey> GetUniqueDocumentKey(Project project, INamedTypeSymbol topLevelNamedType, bool allowDecompilation, CancellationToken cancellationToken)
{
var compilation = await project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
Contract.ThrowIfNull(compilation, "We are trying to produce a key for a language that doesn't support compilations.");

var peMetadataReference = compilation.GetMetadataReference(topLevelNamedType.ContainingAssembly) as PortableExecutableReference;

if (peMetadataReference.FilePath != null)
if (peMetadataReference?.FilePath != null)
{
return new UniqueDocumentKey(peMetadataReference.FilePath, project.Language, SymbolKey.Create(topLevelNamedType, cancellationToken), allowDecompilation);
return new UniqueDocumentKey(peMetadataReference.FilePath, peMetadataReference.GetMetadataId(), project.Language, SymbolKey.Create(topLevelNamedType, cancellationToken), allowDecompilation);
}
else
{
return new UniqueDocumentKey(topLevelNamedType.ContainingAssembly.Identity, project.Language, SymbolKey.Create(topLevelNamedType, cancellationToken), allowDecompilation);
var containingAssembly = topLevelNamedType.ContainingAssembly;
return new UniqueDocumentKey(containingAssembly.Identity, containingAssembly.GetMetadata()?.Id, project.Language, SymbolKey.Create(topLevelNamedType, cancellationToken), allowDecompilation);
}
}

Expand All @@ -288,7 +301,7 @@ private void InitializeWorkspace(Project project)
}
}

internal async Task<SymbolMappingResult> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken)
internal async Task<SymbolMappingResult?> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken)
{
MetadataAsSourceGeneratedFileInfo fileInfo;

Expand Down Expand Up @@ -417,37 +430,41 @@ private class UniqueDocumentKey : IEquatable<UniqueDocumentKey>
/// <summary>
/// The path to the assembly. Null in the case of in-memory assemblies, where we then use assembly identity.
/// </summary>
private readonly string _filePath;
private readonly string? _filePath;

/// <summary>
/// Assembly identity. Only non-null if filePath is null, where it's an in-memory assembly.
/// Assembly identity. Only non-null if <see cref="_filePath"/> is null, where it's an in-memory assembly.
/// </summary>
private readonly AssemblyIdentity _assemblyIdentity;
private readonly AssemblyIdentity? _assemblyIdentity;

private readonly MetadataId? _metadataId;
private readonly string _language;
private readonly SymbolKey _symbolId;
private readonly bool _allowDecompilation;

public UniqueDocumentKey(string filePath, string language, SymbolKey symbolId, bool allowDecompilation)
public UniqueDocumentKey(string filePath, MetadataId? metadataId, string language, SymbolKey symbolId, bool allowDecompilation)
{
Contract.ThrowIfNull(filePath);

_filePath = filePath;
_metadataId = metadataId;
_language = language;
_symbolId = symbolId;
_allowDecompilation = allowDecompilation;
}

public UniqueDocumentKey(AssemblyIdentity assemblyIdentity, string language, SymbolKey symbolId, bool allowDecompilation)
public UniqueDocumentKey(AssemblyIdentity assemblyIdentity, MetadataId? metadataId, string language, SymbolKey symbolId, bool allowDecompilation)
{
Contract.ThrowIfNull(assemblyIdentity);

_assemblyIdentity = assemblyIdentity;
_metadataId = metadataId;
_language = language;
_symbolId = symbolId;
_allowDecompilation = allowDecompilation;
}

public bool Equals(UniqueDocumentKey other)
public bool Equals(UniqueDocumentKey? other)
{
if (other == null)
{
Expand All @@ -456,12 +473,13 @@ public bool Equals(UniqueDocumentKey other)

return StringComparer.OrdinalIgnoreCase.Equals(_filePath, other._filePath) &&
object.Equals(_assemblyIdentity, other._assemblyIdentity) &&
object.Equals(_metadataId, other._metadataId) &&
_language == other._language &&
s_symbolIdComparer.Equals(_symbolId, other._symbolId) &&
_allowDecompilation == other._allowDecompilation;
}

public override bool Equals(object obj)
public override bool Equals(object? obj)
{
return Equals(obj as UniqueDocumentKey);
}
Expand All @@ -470,10 +488,11 @@ public override int GetHashCode()
{
return
Hash.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(_filePath ?? string.Empty),
Hash.Combine(_assemblyIdentity != null ? _assemblyIdentity.GetHashCode() : 0,
Hash.Combine(_language.GetHashCode(),
Hash.Combine(s_symbolIdComparer.GetHashCode(_symbolId),
_allowDecompilation.GetHashCode()))));
Hash.Combine(_assemblyIdentity?.GetHashCode() ?? 0,
Hash.Combine(_metadataId?.GetHashCode() ?? 0,
Hash.Combine(_language.GetHashCode(),
Hash.Combine(s_symbolIdComparer.GetHashCode(_symbolId),
_allowDecompilation.GetHashCode())))));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Collections.Immutable;
using System.IO;
Expand All @@ -20,7 +22,7 @@ internal sealed class MetadataAsSourceGeneratedFileInfo

public readonly string TemporaryFilePath;

private readonly ParseOptions _parseOptions;
private readonly ParseOptions? _parseOptions;

public MetadataAsSourceGeneratedFileInfo(string rootPath, Project sourceProject, INamedTypeSymbol topLevelNamedType, bool allowDecompilation)
{
Expand Down Expand Up @@ -59,7 +61,7 @@ public Tuple<ProjectInfo, DocumentId> GetProjectInfoAndDocumentId(Workspace work
var projectId = ProjectId.CreateNewId();

// Just say it's always a DLL since we probably won't have a Main method
var compilationOptions = workspace.Services.GetLanguageServices(LanguageName).CompilationFactory.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
var compilationOptions = workspace.Services.GetLanguageServices(LanguageName).CompilationFactory!.GetDefaultCompilationOptions().WithOutputKind(OutputKind.DynamicallyLinkedLibrary);

var extension = LanguageName == LanguageNames.CSharp ? ".cs" : ".vb";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using Microsoft.CodeAnalysis.Host;

namespace Microsoft.CodeAnalysis.Editor.Implementation.MetadataAsSource
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Composition;
using System.Threading;
Expand All @@ -26,7 +28,7 @@ public IWorkspaceService CreateService(HostWorkspaceServices workspaceServices)

private sealed class SymbolMappingService : ISymbolMappingService
{
public Task<SymbolMappingResult> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken)
public Task<SymbolMappingResult?> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken)
{
var workspace = document.Project.Solution.Workspace as MetadataAsSourceWorkspace;
if (workspace == null)
Expand All @@ -37,7 +39,7 @@ public Task<SymbolMappingResult> MapSymbolAsync(Document document, SymbolKey sym
return workspace.FileService.MapSymbolAsync(document, symbolId, cancellationToken);
}

public async Task<SymbolMappingResult> MapSymbolAsync(Document document, ISymbol symbol, CancellationToken cancellationToken)
public async Task<SymbolMappingResult?> MapSymbolAsync(Document document, ISymbol symbol, CancellationToken cancellationToken)
{
var compilation = await document.Project.GetCompilationAsync(cancellationToken).ConfigureAwait(false);
return await MapSymbolAsync(document, SymbolKey.Create(symbol, cancellationToken), cancellationToken).ConfigureAwait(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Host;
Expand All @@ -17,7 +19,7 @@ internal interface ISymbolMappingService : IWorkspaceService
/// <param name="symbolId">The id of the symbol to map</param>
/// <param name="cancellationToken">To cancel symbol resolution</param>
/// <returns>The matching symbol from the correct solution or null</returns>
Task<SymbolMappingResult> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken = default);
Task<SymbolMappingResult?> MapSymbolAsync(Document document, SymbolKey symbolId, CancellationToken cancellationToken = default);

/// <summary>
/// Given an <cref see="ISymbol"/> and the document whence the corresponding <cref see="ISymbol"/>
Expand All @@ -28,6 +30,6 @@ internal interface ISymbolMappingService : IWorkspaceService
/// <param name="symbol">The symbol to map</param>
/// <param name="cancellationToken">To cancel symbol resolution</param>
/// <returns>The matching symbol from the correct solution or null</returns>
Task<SymbolMappingResult> MapSymbolAsync(Document document, ISymbol symbol, CancellationToken cancellationToken = default);
Task<SymbolMappingResult?> MapSymbolAsync(Document document, ISymbol symbol, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.SymbolMapping
{
internal class SymbolMappingResult
Expand All @@ -9,6 +13,9 @@ internal class SymbolMappingResult

internal SymbolMappingResult(Project project, ISymbol symbol)
{
Contract.ThrowIfNull(project);
Contract.ThrowIfNull(symbol);

Project = project;
Symbol = symbol;
}
Expand Down

0 comments on commit 5551fad

Please sign in to comment.