提交 89de8217 编写于 作者: T tmeschter

When adding an analyzer, warn if an older version of the assembly has already been loaded.

If an analyzer assembly changes on disk after it has been loaded in VS we put a warning in the Error List letting the user know that they will need to restart VS to use the new version. We do not, however, warn the user in the situation where they add an analyzer to a project but an older version of that file has already been loaded.

This change updates the AnalyzerFileWatcherService to record the creation/modification time when it sees an analyzer assembly being loaded. Later, when an analyzer is added to a project we check if the current creation/modification time of the file matches the recorded time; if it does not, the file does not currently match what was loaded and we need to raise the warning.

We also now tell the AnalyzerFileWatcherService when an analyzer is removed so that the corresponding warnings can be removed from the Error List. (changeset 1410175)
上级 9fd1f729
......@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel.Composition;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
......@@ -18,15 +19,16 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation
[Export(typeof(AnalyzerFileWatcherService))]
internal sealed class AnalyzerFileWatcherService
{
private static object s_analyzerChangedErrorId = new object();
private static readonly object s_analyzerChangedErrorId = new object();
private readonly VisualStudioWorkspaceImpl _workspace;
private readonly HostDiagnosticUpdateSource _updateSource;
private readonly IVsFileChangeEx _fileChangeService;
private readonly Dictionary<string, FileChangeTracker> _fileChangeTrackers = new Dictionary<string, FileChangeTracker>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, DateTime> _assemblyUpdatedTimesUtc = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
private readonly object _fileChangeTrackersLock = new object();
private readonly object _guard = new object();
[ImportingConstructor]
public AnalyzerFileWatcherService(
......@@ -41,9 +43,75 @@ internal sealed class AnalyzerFileWatcherService
AnalyzerFileReference.AssemblyLoad += AnalyzerFileReference_AssemblyLoad;
}
internal void ErrorIfAnalyzerAlreadyLoaded(ProjectId projectId, string analyzerPath)
{
DateTime loadedAssemblyUpdateTimeUtc;
lock (_guard)
{
if (!_assemblyUpdatedTimesUtc.TryGetValue(analyzerPath, out loadedAssemblyUpdateTimeUtc))
{
return;
}
}
DateTime? fileUpdateTimeUtc = GetLastUpdateTimeUtc(analyzerPath);
if (fileUpdateTimeUtc != null &&
loadedAssemblyUpdateTimeUtc != fileUpdateTimeUtc)
{
RaiseAnalyzerChangedWarning(projectId, analyzerPath);
}
}
internal void RemoveAnalyzerAlreadyLoadedDiagnostics(ProjectId projectId, string analyzerPath)
{
_updateSource.ClearDiagnosticsForProject(projectId, Tuple.Create(s_analyzerChangedErrorId, analyzerPath));
}
private void RaiseAnalyzerChangedWarning(ProjectId projectId, string analyzerPath)
{
string id = ServicesVSResources.WRN_AnalyzerChangedId;
string category = ServicesVSResources.ErrorCategory;
string message = string.Format(ServicesVSResources.WRN_AnalyzerChangedMessage, analyzerPath);
DiagnosticData data = new DiagnosticData(
id,
category,
message,
ServicesVSResources.WRN_AnalyzerChangedMessage,
severity: DiagnosticSeverity.Warning,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
warningLevel: 0,
customTags: ImmutableArray<string>.Empty,
workspace: _workspace,
projectId: projectId);
_updateSource.UpdateDiagnosticsForProject(projectId, Tuple.Create(s_analyzerChangedErrorId, analyzerPath), SpecializedCollections.SingletonEnumerable(data));
}
private DateTime? GetLastUpdateTimeUtc(string fullPath)
{
try
{
DateTime creationTimeUtc = File.GetCreationTimeUtc(fullPath);
DateTime writeTimeUtc = File.GetLastWriteTimeUtc(fullPath);
return writeTimeUtc > creationTimeUtc ? writeTimeUtc : creationTimeUtc;
}
catch (IOException)
{
return null;
}
catch (UnauthorizedAccessException)
{
return null;
}
}
private void AnalyzerFileReference_AssemblyLoad(object sender, AnalyzerAssemblyLoadEventArgs e)
{
lock (_fileChangeTrackersLock)
lock (_guard)
{
FileChangeTracker tracker;
if (!_fileChangeTrackers.TryGetValue(e.Path, out tracker))
......@@ -54,6 +122,13 @@ private void AnalyzerFileReference_AssemblyLoad(object sender, AnalyzerAssemblyL
_fileChangeTrackers.Add(e.Path, tracker);
}
DateTime? fileUpdateTime = GetLastUpdateTimeUtc(e.Path);
if (fileUpdateTime.HasValue)
{
_assemblyUpdatedTimesUtc[e.Path] = fileUpdateTime.Value;
}
}
}
......@@ -62,7 +137,7 @@ private void Tracker_UpdatedOnDisk(object sender, EventArgs e)
FileChangeTracker tracker = (FileChangeTracker)sender;
var filePath = tracker.FilePath;
lock (_fileChangeTrackersLock)
lock (_guard)
{
// Once we've created a diagnostic for a given analyzer file, there's
// no need to keep watching it.
......@@ -72,10 +147,6 @@ private void Tracker_UpdatedOnDisk(object sender, EventArgs e)
tracker.Dispose();
tracker.UpdatedOnDisk -= Tracker_UpdatedOnDisk;
string id = ServicesVSResources.WRN_AnalyzerChangedId;
string category = ServicesVSResources.ErrorCategory;
string message = string.Format(ServicesVSResources.WRN_AnalyzerChangedMessage, filePath);
// Traverse the chain of requesting assemblies to get back to the original analyzer
// assembly.
var assemblyPath = filePath;
......@@ -89,20 +160,7 @@ private void Tracker_UpdatedOnDisk(object sender, EventArgs e)
var projectsWithAnalyzer = _workspace.ProjectTracker.Projects.Where(p => p.CurrentProjectAnalyzersContains(assemblyPath)).ToArray();
foreach (var project in projectsWithAnalyzer)
{
DiagnosticData data = new DiagnosticData(
id,
category,
message,
ServicesVSResources.WRN_AnalyzerChangedMessage,
severity: DiagnosticSeverity.Warning,
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true,
warningLevel: 0,
customTags: ImmutableArray<string>.Empty,
workspace: _workspace,
projectId: project.Id);
_updateSource.UpdateDiagnosticsForProject(project.Id, Tuple.Create(s_analyzerChangedErrorId, filePath), SpecializedCollections.SingletonEnumerable(data));
RaiseAnalyzerChangedWarning(project.Id, filePath);
}
}
}
......
......@@ -5,6 +5,7 @@
using System.IO;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Interop;
using Microsoft.VisualStudio.Shell.Interop;
......@@ -12,6 +13,8 @@ namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem
{
internal partial class AbstractProject : IAnalyzerHost
{
private AnalyzerFileWatcherService _analyzerFileWatcherService = null;
public void AddAnalyzerAssembly(string analyzerAssemblyFullPath)
{
if (_analyzers.ContainsKey(analyzerAssemblyFullPath))
......@@ -28,6 +31,8 @@ public void AddAnalyzerAssembly(string analyzerAssemblyFullPath)
var analyzerReference = analyzer.GetReference();
this.ProjectTracker.NotifyWorkspaceHosts(host => host.OnAnalyzerReferenceAdded(_id, analyzerReference));
}
GetAnalyzerFileWatcherService().ErrorIfAnalyzerAlreadyLoaded(_id, analyzerAssemblyFullPath);
}
public void RemoveAnalyzerAssembly(string analyzerAssemblyFullPath)
......@@ -38,6 +43,8 @@ public void RemoveAnalyzerAssembly(string analyzerAssemblyFullPath)
return;
}
GetAnalyzerFileWatcherService().RemoveAnalyzerAlreadyLoadedDiagnostics(_id, analyzerAssemblyFullPath);
_analyzers.Remove(analyzerAssemblyFullPath);
if (_pushingChangesToWorkspaceHosts)
......@@ -127,5 +134,17 @@ private void OnRuleSetFileUpdateOnDisk(object sender, EventArgs e)
ResetAnalyzerRuleSet(filePath);
}
private AnalyzerFileWatcherService GetAnalyzerFileWatcherService()
{
if (_analyzerFileWatcherService == null)
{
var componentModel = (IComponentModel)this.ServiceProvider.GetService(typeof(SComponentModel));
_analyzerFileWatcherService = componentModel.GetService<AnalyzerFileWatcherService>();
}
return _analyzerFileWatcherService;
}
}
}
}
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册