Skip to content

Commit

Permalink
Improve implementation of analyzer assembly loader
Browse files Browse the repository at this point in the history
  • Loading branch information
tmat committed Jun 15, 2016
1 parent f3261c1 commit 02369b0
Show file tree
Hide file tree
Showing 31 changed files with 272 additions and 374 deletions.
2 changes: 1 addition & 1 deletion src/Compilers/CSharp/csc/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public static int Main(string[] args)
=> Main(args, SpecializedCollections.EmptyArray<string>());

public static int Main(string[] args, string[] extraArgs)
=> DesktopBuildClient.Run(args, extraArgs, RequestLanguage.CSharpCompile, Csc.Run, new SimpleAnalyzerAssemblyLoader());
=> DesktopBuildClient.Run(args, extraArgs, RequestLanguage.CSharpCompile, Csc.Run, new DesktopAnalyzerAssemblyLoader());

public static int Run(string[] args, string clientDir, string workingDir, string sdkDir, TextWriter textWriter, IAnalyzerAssemblyLoader analyzerLoader)
=> Csc.Run(args, new BuildPaths(clientDir: clientDir, workingDir: workingDir, sdkDir: sdkDir), textWriter, analyzerLoader);
Expand Down
9 changes: 3 additions & 6 deletions src/Compilers/CSharp/csc/csc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,12 @@
<Compile Include="..\..\Shared\DesktopBuildClient.cs">
<Link>DesktopBuildClient.cs</Link>
</Compile>
<Compile Include="..\..\Shared\AbstractAnalyzerAssemblyLoader.cs">
<Link>AbstractAnalyzerAssemblyLoader.cs</Link>
<Compile Include="..\..\Shared\DesktopAnalyzerAssemblyLoader.cs">
<Link>DesktopAnalyzerAssemblyLoader.cs</Link>
</Compile>
<Compile Include="..\..\Shared\ExitingTraceListener.cs">
<Link>ExitingTraceListener.cs</Link>
</Compile>
<Compile Include="..\..\Shared\SimpleAnalyzerAssemblyLoader.cs">
<Link>SimpleAnalyzerAssemblyLoader.cs</Link>
</Compile>
<Compile Include="..\..\Shared\Csc.cs">
<Link>Csc.cs</Link>
</Compile>
Expand All @@ -71,4 +68,4 @@
<ImportGroup Label="Targets">
<Import Project="..\..\..\..\build\Targets\VSL.Imports.targets" />
</ImportGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public Exception LoadAnalyzer(string analyzerPath)

public class AnalyzerFileReferenceTests : TestBase
{
private static readonly SimpleAnalyzerAssemblyLoader s_analyzerLoader = new SimpleAnalyzerAssemblyLoader();
private static readonly DesktopAnalyzerAssemblyLoader s_analyzerLoader = new DesktopAnalyzerAssemblyLoader();

public static AnalyzerFileReference CreateAnalyzerFileReference(string fullPath)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<Compile Include="PEWriter\PEBuilderTests.cs" />
<Compile Include="PEWriter\UsedNamespaceOrTypeTests.cs" />
<Compile Include="RealParserTests.cs" />
<Compile Include="SimpleAnalyzerAssemblyLoaderTests.cs" />
<Compile Include="DesktopAnalyzerAssemblyLoaderTests.cs" />
<Compile Include="SourceFileResolverTest.cs" />
<Compile Include="SourceGeneratorTests.cs" />
<Compile Include="Text\LargeTextTests.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,31 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;

namespace Microsoft.CodeAnalysis.UnitTests
{
public sealed class SimpleAnalyzerAssemblyLoaderTests : TestBase
public sealed class DesktopAnalyzerAssemblyLoaderTests : TestBase
{
[Fact]
public void AddDependencyLocationThrowsOnNull()
{
var loader = new SimpleAnalyzerAssemblyLoader();
var loader = new DesktopAnalyzerAssemblyLoader();

Assert.Throws<ArgumentNullException>("fullPath", () => loader.AddDependencyLocation(null));
Assert.Throws<ArgumentException>("fullPath", () => loader.AddDependencyLocation("a"));
}

[Fact]
public void ThrowsForMissingFile()
{
var path = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".dll");

var loader = new SimpleAnalyzerAssemblyLoader();
var loader = new DesktopAnalyzerAssemblyLoader();

Assert.ThrowsAny<Exception>(() => loader.LoadFromPath(path));
}
Expand All @@ -40,7 +37,7 @@ public void BasicLoad()

var alphaDll = directory.CreateFile("Alpha.dll").WriteAllBytes(TestResources.AssemblyLoadTests.Alpha);

var loader = new SimpleAnalyzerAssemblyLoader();
var loader = new DesktopAnalyzerAssemblyLoader();

Assembly alpha = loader.LoadFromPath(alphaDll.Path);

Expand All @@ -58,7 +55,7 @@ public void AssemblyLoading()
var gammaDll = Temp.CreateDirectory().CreateFile("Gamma.dll").WriteAllBytes(TestResources.AssemblyLoadTests.Gamma);
var deltaDll = Temp.CreateDirectory().CreateFile("Delta.dll").WriteAllBytes(TestResources.AssemblyLoadTests.Delta);

var loader = new SimpleAnalyzerAssemblyLoader();
var loader = new DesktopAnalyzerAssemblyLoader();
loader.AddDependencyLocation(alphaDll.Path);
loader.AddDependencyLocation(betaDll.Path);
loader.AddDependencyLocation(gammaDll.Path);
Expand Down
1 change: 0 additions & 1 deletion src/Compilers/Core/CommandLine/CommandLine.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
<Import_RootNamespace>CommandLine</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AssemblyIdentityUtils.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BuildProtocol.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BuildClient.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ConsoleUtil.cs" />
Expand Down
4 changes: 3 additions & 1 deletion src/Compilers/Core/Portable/CodeAnalysis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@
<Compile Include="CaseInsensitiveComparison.cs" />
<Compile Include="CodeGen\ILEmitStyle.cs" />
<Compile Include="DiagnosticAnalyzer\AnalysisResult.cs" />
<Compile Include="DiagnosticAnalyzer\AnalyzerAssemblyLoader.cs" />
<Compile Include="FileSystem\CompilerPathUtilities.cs" />
<Compile Include="InternalUtilities\AssemblyIdentityUtils.cs" />
<Compile Include="InternalUtilities\EmptyComparer.cs" />
<Compile Include="InternalUtilities\StringOrdinalComparer.cs" />
<Compile Include="MetadataReference\AssemblyIdentityMap.cs" />
Expand Down Expand Up @@ -211,7 +214,6 @@
<Compile Include="Compilation\LoadDirective.cs" />
<Compile Include="Compilation\CommonSyntaxAndDeclarationManager.cs" />
<Compile Include="Compilation\SymbolFilter.cs" />
<Compile Include="CompilerPathUtilities.cs" />
<Compile Include="DiagnosticAnalyzer\AnalysisResultBuilder.cs" />
<Compile Include="DiagnosticAnalyzer\AnalysisScope.cs" />
<Compile Include="DiagnosticAnalyzer\AnalysisState.AnalyzerStateData.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis
{
internal abstract class AnalyzerAssemblyLoader : IAnalyzerAssemblyLoader
{
private readonly object _guard = new object();

// lock _guard to read/write
private readonly Dictionary<string, Assembly> _loadedAssembliesByPath = new Dictionary<string, Assembly>();
private readonly Dictionary<AssemblyIdentity, Assembly> _loadedAssembliesByIdentity = new Dictionary<AssemblyIdentity, Assembly>();

// maps file name to a full path (lock _guard to read/write):
private readonly Dictionary<string, List<string>> _knownAssemblyPathsBySimpleName = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);

protected abstract Assembly LoadFromPathImpl(string fullPath);

#region Public API

public void AddDependencyLocation(string fullPath)
{
CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
string simpleName = PathUtilities.GetFileName(fullPath, includeExtension: false);

lock (_guard)
{
List<string> paths;
if (!_knownAssemblyPathsBySimpleName.TryGetValue(simpleName, out paths))
{
_knownAssemblyPathsBySimpleName.Add(simpleName, new List<string>() { fullPath });
}
else if (!paths.Contains(fullPath))
{
paths.Add(fullPath);
}
}
}

public Assembly LoadFromPath(string fullPath)
{
CompilerPathUtilities.RequireAbsolutePath(fullPath, nameof(fullPath));
return LoadFromPathUnchecked(fullPath);
}

#endregion

private Assembly LoadFromPathUnchecked(string fullPath)
{
Debug.Assert(PathUtilities.IsAbsolute(fullPath));

lock (_guard)
{
Assembly existingAssembly;
if (_loadedAssembliesByPath.TryGetValue(fullPath, out existingAssembly))
{
return existingAssembly;
}
}

Assembly assembly = LoadFromPathImpl(fullPath);
return AddToCache(assembly, fullPath);
}

private Assembly AddToCache(Assembly assembly, string fullPath)
{
Debug.Assert(PathUtilities.IsAbsolute(fullPath));
Debug.Assert(assembly != null);

var identity = AssemblyIdentity.FromAssemblyDefinition(assembly);

lock (_guard)
{
// The same assembly may be loaded from two different full paths (e.g. when loaded from GAC, etc.),
// or another thread might have loaded the assembly after we checked above.
Assembly existingAssembly;
if (_loadedAssembliesByIdentity.TryGetValue(identity, out existingAssembly))
{
return existingAssembly;
}

_loadedAssembliesByIdentity.Add(identity, assembly);

// An assembly file might be replaced by another file with a different identity.
// Last one wins.
_loadedAssembliesByPath[fullPath] = assembly;

return assembly;
}
}

public Assembly Load(string displayName)
{
AssemblyIdentity requestedIdentity;
if (!AssemblyIdentity.TryParseDisplayName(displayName, out requestedIdentity))
{
return null;
}

ImmutableArray<string> candidatePaths;
lock (_guard)
{
Assembly existingAssembly;

// First, check if this loader already loaded the requested assembly:
if (_loadedAssembliesByIdentity.TryGetValue(requestedIdentity, out existingAssembly))
{
return existingAssembly;
}

// Second, check if an assembly file of the same simple name was registered with the loader:
List<string> pathList;
if (!_knownAssemblyPathsBySimpleName.TryGetValue(requestedIdentity.Name, out pathList))
{
return null;
}

Debug.Assert(pathList.Count > 0);
candidatePaths = pathList.ToImmutableArray();
}

// Multiple assemblies of the same simple name but different identities might have been registered.
// Load the one that matches the requested identity (if any).
foreach (var candidatePath in candidatePaths)
{
var candidateIdentity = AssemblyIdentityUtils.TryGetAssemblyIdentity(candidatePath);

if (requestedIdentity.Equals(candidateIdentity))
{
return LoadFromPathUnchecked(candidatePath);
}
}

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

using System;
using System.Reflection;

namespace Microsoft.CodeAnalysis
Expand Down Expand Up @@ -29,11 +30,15 @@ public interface IAnalyzerAssemblyLoader
/// Multiple calls with the same path should return the same
/// <see cref="Assembly"/> instance.
/// </remarks>
/// <exception cref="ArgumentNullException"><paramref name="fullPath" /> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="fullPath" /> is not a full path.</exception>
Assembly LoadFromPath(string fullPath);

/// <summary>
/// Adds a file to consider when loading an analyzer or its dependencies.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="fullPath" /> is null.</exception>
/// <exception cref="ArgumentException"><paramref name="fullPath" /> is not a full path.</exception>
void AddDependencyLocation(string fullPath);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@

using System;
using System.Collections.Immutable;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CommandLine
namespace Microsoft.CodeAnalysis
{
internal static class AssemblyIdentityUtils
{
public static AssemblyIdentity TryGetAssemblyIdentity(string filePath)
{
try
{
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
using (var stream = PortableShim.FileStream.Create(filePath, PortableShim.FileMode.Open, PortableShim.FileAccess.Read, PortableShim.FileShare.ReadWriteBitwiseOrDelete))
using (var peReader = new PEReader(stream))
{
var metadataReader = peReader.GetMetadataReader();
Expand Down
4 changes: 2 additions & 2 deletions src/Compilers/Server/VBCSCompiler/VBCSCompiler.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@
<Reference Include="System.Core" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\Shared\AbstractAnalyzerAssemblyLoader.cs">
<Link>AbstractAnalyzerAssemblyLoader.cs</Link>
<Compile Include="..\..\Shared\DesktopAnalyzerAssemblyLoader.cs">
<Link>DesktopAnalyzerAssemblyLoader.cs</Link>
</Compile>
<Compile Include="..\..\Shared\DesktopBuildClient.cs">
<Link>DesktopBuildClient.cs</Link>
Expand Down
Loading

0 comments on commit 02369b0

Please sign in to comment.