提交 04e4d15d 编写于 作者: T tmeschter

Warn if analyzers have dependencies with the same identity but different contents.

Consider what happens if you have two analyzers, A and B, that each depend on an assembly named C.

  Directory 1:
    A.dll
    C.dll

  Directory 2:
    B.dll
    C.dll

If both copies of C have the same identity (name, version, culture, public key token, etc.) then only one of them is actually going to be loaded into VS. If both copies are identical then it doesn't matter, but if their contents differ the analyzers may not work the way they are supposed to or may fail outright.

Here we attempt to let the user know that this might happen. Whenever an analyzer is added or removed we identify the transitive set of assemblies the analyzers' may load and identify assemblies that have the same identity. We then hash the file contents of these assemblies and compare. If they are different we surface a conflict in the Error List.

In solutions with many different analyzers or dependencies this is a potentially expensive operation. To limit the impact on the system we only allow one of these operations to run at a time. If an analyzer is added or removed while a check is in progress we immediately signal for cancellation, wait for the existing task to finish up, and only then start the new one. (changeset 1411056)
上级 9065cff1
// 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.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security;
using System.Threading;
using Microsoft.CodeAnalysis;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation
{
internal sealed class AnalyzerDependencyChecker
{
private readonly ImmutableHashSet<string> _analyzerFilePaths;
private readonly SortedSet<string> _examinedFilePaths = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly SortedSet<string> _filePathsToExamine = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _dependencyPathToAnalyzerPathMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public AnalyzerDependencyChecker(IEnumerable<string> analyzerFilePaths)
{
_analyzerFilePaths = analyzerFilePaths.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
}
public ImmutableArray<AnalyzerDependencyConflict> Run(CancellationToken cancellationToken = default(CancellationToken))
{
foreach (var analyzerFilePath in _analyzerFilePaths)
{
cancellationToken.ThrowIfCancellationRequested();
if (File.Exists(analyzerFilePath))
{
AddDependenciesToWorkList(analyzerFilePath, analyzerFilePath);
}
}
List<DependencyInfo> dependencies = new List<DependencyInfo>();
while (_filePathsToExamine.Count > 0)
{
cancellationToken.ThrowIfCancellationRequested();
string filePath = _filePathsToExamine.Min;
_filePathsToExamine.Remove(filePath);
_examinedFilePaths.Add(filePath);
AssemblyIdentity assemblyIdentity = TryReadAssemblyIdentity(filePath);
if (assemblyIdentity != null)
{
var analyzerPath = _dependencyPathToAnalyzerPathMap[filePath];
dependencies.Add(new DependencyInfo(filePath, assemblyIdentity, analyzerPath));
AddDependenciesToWorkList(analyzerPath, filePath);
}
}
ImmutableArray<AnalyzerDependencyConflict>.Builder conflicts = ImmutableArray.CreateBuilder<AnalyzerDependencyConflict>();
foreach (var identityGroup in dependencies.GroupBy(di => di.Identity))
{
var identityGroupArray = identityGroup.ToImmutableArray();
for (int i = 0; i < identityGroupArray.Length; i++)
{
for (int j = i + 1; j < identityGroupArray.Length; j++)
{
cancellationToken.ThrowIfCancellationRequested();
byte[] hash1;
byte[] hash2;
if ((hash1 = identityGroupArray[i].TryGetFileHash()) != null &&
(hash2 = identityGroupArray[j].TryGetFileHash()) != null &&
!HashesAreEqual(hash1, hash2))
{
conflicts.Add(new AnalyzerDependencyConflict(
identityGroupArray[i].DependencyFilePath,
identityGroupArray[j].DependencyFilePath,
identityGroupArray[i].AnalyzerFilePath,
identityGroupArray[j].AnalyzerFilePath));
}
}
}
}
return conflicts.ToImmutable();
}
private bool HashesAreEqual(byte[] hash1, byte[] hash2)
{
for (int i = 0; i < hash1.Length; i++)
{
if (hash1[i] != hash2[i])
{
return false;
}
}
return true;
}
private void AddDependenciesToWorkList(string analyzerFilePath, string assemblyPath)
{
ImmutableArray<string> referencedAssemblyNames = GetReferencedAssemblyNames(assemblyPath);
foreach (var reference in referencedAssemblyNames)
{
string referenceFilePath = Path.Combine(Path.GetDirectoryName(analyzerFilePath), reference + ".dll");
if (!_examinedFilePaths.Contains(referenceFilePath) &&
File.Exists(referenceFilePath))
{
_filePathsToExamine.Add(referenceFilePath);
_dependencyPathToAnalyzerPathMap[referenceFilePath] = analyzerFilePath;
}
}
}
private ImmutableArray<string> GetReferencedAssemblyNames(string assemblyPath)
{
try
{
using (var stream = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
using (var peReader = new PEReader(stream))
{
var metadataReader = peReader.GetMetadataReader();
var builder = ImmutableArray.CreateBuilder<string>();
foreach (var referenceHandle in metadataReader.AssemblyReferences)
{
var reference = metadataReader.GetAssemblyReference(referenceHandle);
builder.Add(metadataReader.GetString(reference.Name));
}
return builder.ToImmutable();
}
}
catch { }
return ImmutableArray<string>.Empty;
}
private AssemblyIdentity TryReadAssemblyIdentity(string filePath)
{
try
{
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
using (var peReader = new PEReader(stream))
{
var metadataReader = peReader.GetMetadataReader();
string name;
Version version;
string cultureName;
ImmutableArray<byte> publicKeyToken;
var assemblyDefinition = metadataReader.GetAssemblyDefinition();
name = metadataReader.GetString(assemblyDefinition.Name);
version = assemblyDefinition.Version;
cultureName = metadataReader.GetString(assemblyDefinition.Culture);
publicKeyToken = metadataReader.GetBlobContent(assemblyDefinition.PublicKey);
return new AssemblyIdentity(name, version, cultureName, publicKeyToken, hasPublicKey: false);
}
}
catch { }
return null;
}
private sealed class DependencyInfo
{
private byte[] _lazyFileHash;
private bool _triedToComputeFileHash = false;
public DependencyInfo(string dependencyFilePath, AssemblyIdentity identity, string analyzerFilePath)
{
DependencyFilePath = dependencyFilePath;
Identity = identity;
AnalyzerFilePath = analyzerFilePath;
}
public string AnalyzerFilePath { get; }
public string DependencyFilePath { get; }
public AssemblyIdentity Identity { get; }
public byte[] TryGetFileHash()
{
if (!_triedToComputeFileHash)
{
_triedToComputeFileHash = true;
_lazyFileHash = TryComputeFileHash(DependencyFilePath);
}
return _lazyFileHash;
}
private byte[] TryComputeFileHash(string filePath)
{
try
{
using (var cryptoProvider = new SHA1CryptoServiceProvider())
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
{
return cryptoProvider.ComputeHash(stream);
}
}
catch { }
return null;
}
}
}
}
\ No newline at end of file
// 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.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation
{
[Export(typeof(AnalyzerDependencyCheckingService))]
internal sealed class AnalyzerDependencyCheckingService
{
private static readonly object s_dependencyConflictErrorId = new object();
private readonly VisualStudioWorkspaceImpl _workspace;
private readonly HostDiagnosticUpdateSource _updateSource;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private Task<ImmutableArray<AnalyzerDependencyConflict>> _task = Task.FromResult(ImmutableArray<AnalyzerDependencyConflict>.Empty);
private ImmutableHashSet<string> _analyzerPaths = ImmutableHashSet.Create<string>(StringComparer.OrdinalIgnoreCase);
[ImportingConstructor]
public AnalyzerDependencyCheckingService(
VisualStudioWorkspaceImpl workspace,
HostDiagnosticUpdateSource updateSource)
{
_workspace = workspace;
_updateSource = updateSource;
}
public async void CheckForConflictsAsync()
{
try
{
ImmutableArray<AnalyzerDependencyConflict> conflicts = await GetConflictsAsync().ConfigureAwait(continueOnCapturedContext: true);
var builder = ImmutableArray.CreateBuilder<DiagnosticData>();
foreach (var project in _workspace.ProjectTracker.Projects)
{
builder.Clear();
foreach (var conflict in conflicts)
{
if (project.CurrentProjectAnalyzersContains(conflict.AnalyzerFilePath1) ||
project.CurrentProjectAnalyzersContains(conflict.AnalyzerFilePath2))
{
builder.Add(CreateDiagnostic(project.Id, conflict));
}
}
_updateSource.UpdateDiagnosticsForProject(project.Id, s_dependencyConflictErrorId, builder.ToImmutable());
}
foreach (var conflict in conflicts)
{
LogConflict(conflict);
}
}
catch (OperationCanceledException) { }
}
private void LogConflict(AnalyzerDependencyConflict conflict)
{
Logger.Log(
FunctionId.AnalyzerDependencyCheckingService_CheckForConflictsAsync,
KeyValueLogMessage.Create(m =>
{
m["Dependency1"] = Path.GetFileName(conflict.DependencyFilePath1);
m["Dependency2"] = Path.GetFileName(conflict.DependencyFilePath2);
m["Analyzer1"] = Path.GetFileName(conflict.AnalyzerFilePath1);
m["Analyzer2"] = Path.GetFileName(conflict.AnalyzerFilePath2);
}));
}
private DiagnosticData CreateDiagnostic(ProjectId projectId, AnalyzerDependencyConflict conflict)
{
string id = ServicesVSResources.WRN_AnalyzerDependencyConflictId;
string category = ServicesVSResources.ErrorCategory;
string message = string.Format(
ServicesVSResources.WRN_AnalyzerDependencyConflictMessage,
conflict.DependencyFilePath1,
Path.GetFileNameWithoutExtension(conflict.AnalyzerFilePath1),
conflict.DependencyFilePath2,
Path.GetFileNameWithoutExtension(conflict.AnalyzerFilePath2));
DiagnosticData data = new DiagnosticData(
id,
category,
message,
ServicesVSResources.WRN_AnalyzerDependencyConflictMessage,
severity: DiagnosticSeverity.Warning,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
warningLevel: 0,
customTags: ImmutableArray<string>.Empty,
workspace: _workspace,
projectId: projectId);
return data;
}
private Task<ImmutableArray<AnalyzerDependencyConflict>> GetConflictsAsync()
{
ImmutableHashSet<string> currentAnalyzerPaths = _workspace.CurrentSolution
.Projects
.SelectMany(p => p.AnalyzerReferences)
.OfType<AnalyzerFileReference>()
.Select(a => a.FullPath)
.ToImmutableHashSet(StringComparer.OrdinalIgnoreCase);
if (currentAnalyzerPaths.SetEquals(_analyzerPaths))
{
return _task;
}
_cancellationTokenSource.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
_analyzerPaths = currentAnalyzerPaths;
_task = _task.SafeContinueWith(_ =>
{
return new AnalyzerDependencyChecker(currentAnalyzerPaths).Run(_cancellationTokenSource.Token);
},
TaskScheduler.Default);
return _task;
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.VisualStudio.LanguageServices.Implementation
{
internal sealed class AnalyzerDependencyConflict
{
public AnalyzerDependencyConflict(string dependencyFilePath1, string dependencyFilePath2, string analyzerFilePath1, string analyzerFilePath2)
{
DependencyFilePath1 = dependencyFilePath1;
DependencyFilePath2 = dependencyFilePath2;
AnalyzerFilePath1 = analyzerFilePath1;
AnalyzerFilePath2 = analyzerFilePath2;
}
public string DependencyFilePath1 { get; }
public string DependencyFilePath2 { get; }
public string AnalyzerFilePath1 { get; }
public string AnalyzerFilePath2 { get; }
}
}
......@@ -14,6 +14,7 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
internal partial class AbstractProject : IAnalyzerHost
{
private AnalyzerFileWatcherService _analyzerFileWatcherService = null;
private AnalyzerDependencyCheckingService _dependencyCheckingService = null;
public void AddAnalyzerAssembly(string analyzerAssemblyFullPath)
{
......@@ -30,6 +31,8 @@ public void AddAnalyzerAssembly(string analyzerAssemblyFullPath)
{
var analyzerReference = analyzer.GetReference();
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAnalyzerReferenceAdded(_id, analyzerReference));
GetAnalyzerDependencyCheckingService().CheckForConflictsAsync();
}
GetAnalyzerFileWatcherService().ErrorIfAnalyzerAlreadyLoaded(_id, analyzerAssemblyFullPath);
......@@ -51,6 +54,8 @@ public void RemoveAnalyzerAssembly(string analyzerAssemblyFullPath)
{
var analyzerReference = analyzer.GetReference();
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAnalyzerReferenceRemoved(_id, analyzerReference));
GetAnalyzerDependencyCheckingService().CheckForConflictsAsync();
}
analyzer.Dispose();
......@@ -146,5 +151,17 @@ private AnalyzerFileWatcherService GetAnalyzerFileWatcherService()
return _analyzerFileWatcherService;
}
private AnalyzerDependencyCheckingService GetAnalyzerDependencyCheckingService()
{
if (_dependencyCheckingService == null)
{
var componentModel = (IComponentModel)this.ServiceProvider.GetService(typeof(SComponentModel));
_dependencyCheckingService = componentModel.GetService<AnalyzerDependencyCheckingService>();
}
return _dependencyCheckingService;
}
}
}
\ No newline at end of file
}
......@@ -993,6 +993,24 @@ internal class ServicesVSResources {
}
}
/// <summary>
/// Looks up a localized string similar to AnalyzerDependencyConflict.
/// </summary>
internal static string WRN_AnalyzerDependencyConflictId {
get {
return ResourceManager.GetString("WRN_AnalyzerDependencyConflictId", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Assembly &apos;{0}&apos; used by analyzer &apos;{1}&apos; and assembly &apos;{2}&apos; used by analyzer &apos;{3}&apos; have the same identity but different contents. These analyzers may not run correctly..
/// </summary>
internal static string WRN_AnalyzerDependencyConflictMessage {
get {
return ResourceManager.GetString("WRN_AnalyzerDependencyConflictMessage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The assembly {0} does not contain any analyzers..
/// </summary>
......
......@@ -441,4 +441,10 @@ Use the dropdown to view and switch to other projects this file may belong to.</
<data name="PreviewChangesProjectReference" xml:space="preserve">
<value>Project reference to '{0}' in project '{1}'</value>
</data>
<data name="WRN_AnalyzerDependencyConflictId" xml:space="preserve">
<value>AnalyzerDependencyConflict</value>
</data>
<data name="WRN_AnalyzerDependencyConflictMessage" xml:space="preserve">
<value>Assembly '{0}' used by analyzer '{1}' and assembly '{2}' used by analyzer '{3}' have the same identity but different contents. These analyzers may not run correctly.</value>
</data>
</root>
\ No newline at end of file
......@@ -18,6 +18,9 @@
</PropertyGroup>
<ItemGroup Label="Build Items">
<Compile Include="..\..\..\Compilers\Core\Desktop\IVsSQM.cs" />
<Compile Include="Implementation\AnalyzerDependencyChecker.cs" />
<Compile Include="Implementation\AnalyzerDependencyCheckingService.cs" />
<Compile Include="Implementation\AnalyzerDependencyConflict.cs" />
<Compile Include="Implementation\CompilationErrorTelemetry\CompilationErrorTelemetryIncrementalAnalyzer.cs" />
<Compile Include="Implementation\Diagnostics\VisualStudioVenusSpanMappingService.cs" />
<Compile Include="Implementation\Preview\ReferenceChange.MetadataReferenceChange.cs" />
......
......@@ -198,6 +198,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="AbstractTextViewFilterTests.vb" />
<Compile Include="AnalyzerSupport\AnalyzerDependencyCheckerTests.vb" />
<Compile Include="ChangeSignature\ChangeSignatureViewModelTests.vb" />
<Compile Include="ChangeSignature\ChangeSignatureViewModelTestState.vb" />
<Compile Include="CodeModel\AbstractCodeAttributeTests.vb" />
......
......@@ -297,5 +297,6 @@ internal enum FunctionId
Tagger_Diagnostics_Updated,
SuggestedActions_HasSuggestedActionsAsync,
SuggestedActions_GetSuggestedActions,
AnalyzerDependencyCheckingService_CheckForConflictsAsync
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册