提交 a41e5c9f 编写于 作者: H Heejae Chang

Merge pull request #11367 from heejaechang/ProjectError

make us to accept project level build errors as well.
......@@ -99,7 +99,8 @@ public async Task<AnalysisResult> GetAnalysisDataAsync(Project project, bool avo
if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false))
{
Contract.Requires(false, "How this can happen?");
// this can happen if SaveAsync is not yet called but active file merge happened. one of case is if user did build before the very first
// analysis happened.
}
return builder.ToResult();
......@@ -178,7 +179,8 @@ public async Task<AnalysisResult> GetProjectAnalysisDataAsync(Project project, b
if (!await TryDeserializeAsync(serializer, project, project.Id, _owner.NonLocalStateName, builder.AddOthers, cancellationToken).ConfigureAwait(false))
{
Contract.Requires(false, "How this can happen?");
// this can happen if SaveAsync is not yet called but active file merge happened. one of case is if user did build before the very first
// analysis happened.
}
return builder.ToResult();
......
......@@ -76,6 +76,11 @@ public ImmutableArray<DiagnosticData> GetBuildErrors()
return _lastBuiltResult;
}
public bool SupportedDiagnosticId(ProjectId projectId, string id)
{
return _state?.SupportedDiagnosticId(projectId, id) ?? false;
}
public void ClearErrors(ProjectId projectId)
{
// capture state if it exists
......@@ -88,7 +93,7 @@ public void ClearErrors(ProjectId projectId)
// otherwise (such as closing solution or removing project), no need to record it
state?.Built(projectId);
ClearProjectErrors(projectId);
ClearProjectErrors(state?.Solution ?? _workspace.CurrentSolution, projectId);
}).CompletesAsyncOperation(asyncToken);
}
......@@ -102,7 +107,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
case WorkspaceChangeKind.SolutionReloaded:
{
var asyncToken = _listener.BeginAsyncOperation("OnSolutionChanged");
_taskQueue.ScheduleTask(() => e.OldSolution.ProjectIds.Do(p => ClearProjectErrors(p, e.OldSolution))).CompletesAsyncOperation(asyncToken);
_taskQueue.ScheduleTask(() => e.OldSolution.ProjectIds.Do(p => ClearProjectErrors(e.OldSolution, p))).CompletesAsyncOperation(asyncToken);
break;
}
......@@ -110,7 +115,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
case WorkspaceChangeKind.ProjectReloaded:
{
var asyncToken = _listener.BeginAsyncOperation("OnProjectChanged");
_taskQueue.ScheduleTask(() => ClearProjectErrors(e.ProjectId, e.OldSolution)).CompletesAsyncOperation(asyncToken);
_taskQueue.ScheduleTask(() => ClearProjectErrors(e.OldSolution, e.ProjectId)).CompletesAsyncOperation(asyncToken);
break;
}
......@@ -118,7 +123,7 @@ private void OnWorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
case WorkspaceChangeKind.DocumentReloaded:
{
var asyncToken = _listener.BeginAsyncOperation("OnDocumentRemoved");
_taskQueue.ScheduleTask(() => ClearDocumentErrors(e.ProjectId, e.DocumentId)).CompletesAsyncOperation(asyncToken);
_taskQueue.ScheduleTask(() => ClearDocumentErrors(e.OldSolution, e.ProjectId, e.DocumentId)).CompletesAsyncOperation(asyncToken);
break;
}
......@@ -169,16 +174,11 @@ internal void OnSolutionBuild(object sender, UIContextChangedEventArgs e)
// pause live analyzer
using (var operation = _notificationService.Start("BuildDone"))
{
// we will have a race here since we can't track version of solution the out of proc build actually used.
// result of the race will be us dropping some diagnostics from the build to the floor.
var solution = _workspace.CurrentSolution;
var supportedIdMap = GetSupportedLiveDiagnosticId(solution, inprogressState);
Func<DiagnosticData, bool> liveDiagnosticChecker = d =>
{
// REVIEW: we probably need a better design on de-duplicating live and build errors. or don't de-dup at all.
// for now, we are special casing compiler error case.
var project = solution.GetProject(d.ProjectId);
var project = inprogressState.Solution.GetProject(d.ProjectId);
if (project == null)
{
// project doesn't exist
......@@ -194,8 +194,7 @@ internal void OnSolutionBuild(object sender, UIContextChangedEventArgs e)
return false;
}
HashSet<string> set;
if (supportedIdMap.TryGetValue(d.ProjectId, out set) && set.Contains(d.Id))
if (inprogressState.SupportedDiagnosticId(d.ProjectId, d.Id))
{
return true;
}
......@@ -206,8 +205,8 @@ internal void OnSolutionBuild(object sender, UIContextChangedEventArgs e)
var diagnosticService = _diagnosticService as DiagnosticAnalyzerService;
if (diagnosticService != null)
{
await CleanupAllLiveErrorsIfNeededAsync(diagnosticService, solution, inprogressState).ConfigureAwait(false);
await SyncBuildErrorsAndReportAsync(diagnosticService, inprogressState.GetLiveDiagnosticsPerProject(liveDiagnosticChecker)).ConfigureAwait(false);
await CleanupAllLiveErrorsIfNeededAsync(diagnosticService, inprogressState.Solution, inprogressState).ConfigureAwait(false);
await SyncBuildErrorsAndReportAsync(diagnosticService, inprogressState.Solution, inprogressState.GetLiveDiagnosticsPerProject(liveDiagnosticChecker)).ConfigureAwait(false);
}
inprogressState.Done();
......@@ -239,7 +238,8 @@ private System.Threading.Tasks.Task CleanupAllLiveErrors(DiagnosticAnalyzerServi
return diagnosticService.SynchronizeWithBuildAsync(_workspace, map);
}
private async System.Threading.Tasks.Task SyncBuildErrorsAndReportAsync(DiagnosticAnalyzerService diagnosticService, ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticData>> map)
private async System.Threading.Tasks.Task SyncBuildErrorsAndReportAsync(
DiagnosticAnalyzerService diagnosticService, Solution solution, ImmutableDictionary<ProjectId, ImmutableArray<DiagnosticData>> map)
{
// make those errors live errors
await diagnosticService.SynchronizeWithBuildAsync(_workspace, map).ConfigureAwait(false);
......@@ -253,56 +253,36 @@ private async System.Threading.Tasks.Task SyncBuildErrorsAndReportAsync(Diagnost
foreach (var projectGroup in group.GroupBy(g => g.ProjectId))
{
Contract.ThrowIfNull(projectGroup.Key);
ReportBuildErrors(projectGroup.Key, projectGroup.ToImmutableArray());
ReportBuildErrors(projectGroup.Key, solution, projectGroup.ToImmutableArray());
}
continue;
}
ReportBuildErrors(group.Key, group.ToImmutableArray());
ReportBuildErrors(group.Key, solution, group.ToImmutableArray());
}
}
private void ReportBuildErrors<T>(T item, ImmutableArray<DiagnosticData> buildErrors)
private void ReportBuildErrors<T>(T item, Solution solution, ImmutableArray<DiagnosticData> buildErrors)
{
var projectId = item as ProjectId;
if (projectId != null)
{
RaiseDiagnosticsCreated(projectId, projectId, null, buildErrors);
RaiseDiagnosticsCreated(projectId, solution, projectId, null, buildErrors);
return;
}
// must be not null
var documentId = item as DocumentId;
RaiseDiagnosticsCreated(documentId, documentId.ProjectId, documentId, buildErrors);
}
private Dictionary<ProjectId, HashSet<string>> GetSupportedLiveDiagnosticId(Solution solution, InprogressState state)
{
var map = new Dictionary<ProjectId, HashSet<string>>();
// here, we don't care about perf that much since build is already expensive work
foreach (var projectId in state.GetProjectsWithErrors(solution))
{
var project = solution.GetProject(projectId);
if (project == null)
{
continue;
}
var descriptorMap = _diagnosticService.GetDiagnosticDescriptors(project);
map.Add(project.Id, new HashSet<string>(descriptorMap.Values.SelectMany(v => v.Select(d => d.Id))));
}
return map;
RaiseDiagnosticsCreated(documentId, solution, documentId.ProjectId, documentId, buildErrors);
}
private void ClearProjectErrors(ProjectId projectId, Solution solution = null)
private void ClearProjectErrors(Solution solution, ProjectId projectId)
{
// remove all project errors
RaiseDiagnosticsRemoved(projectId, projectId, documentId: null);
RaiseDiagnosticsRemoved(projectId, solution, projectId, documentId: null);
var project = (solution ?? _workspace.CurrentSolution).GetProject(projectId);
var project = solution.GetProject(projectId);
if (project == null)
{
return;
......@@ -311,13 +291,25 @@ private void ClearProjectErrors(ProjectId projectId, Solution solution = null)
// remove all document errors
foreach (var documentId in project.DocumentIds)
{
ClearDocumentErrors(projectId, documentId);
ClearDocumentErrors(solution, projectId, documentId);
}
}
private void ClearDocumentErrors(ProjectId projectId, DocumentId documentId)
private void ClearDocumentErrors(Solution solution, ProjectId projectId, DocumentId documentId)
{
RaiseDiagnosticsRemoved(documentId, projectId, documentId);
RaiseDiagnosticsRemoved(documentId, solution, projectId, documentId);
}
public void AddNewErrors(ProjectId projectId, DiagnosticData diagnostic)
{
// capture state that will be processed in background thread.
var state = GetOrCreateInprogressState();
var asyncToken = _listener.BeginAsyncOperation("Project New Errors");
_taskQueue.ScheduleTask(() =>
{
state.AddError(projectId, diagnostic);
}).CompletesAsyncOperation(asyncToken);
}
public void AddNewErrors(DocumentId documentId, DiagnosticData diagnostic)
......@@ -354,22 +346,25 @@ private InprogressState GetOrCreateInprogressState()
{
if (_state == null)
{
Interlocked.CompareExchange(ref _state, new InprogressState(this), null);
// here, we take current snapshot of solution when the state is first created. and through out this code, we use this snapshot.
// since we have no idea what actual snapshot of solution the out of proc build has picked up, it doesn't remove the race we can have
// between build and diagnostic service, but this at least make us to consistent inside of our code.
Interlocked.CompareExchange(ref _state, new InprogressState(this, _workspace.CurrentSolution), null);
}
return _state;
}
private void RaiseDiagnosticsCreated(object id, ProjectId projectId, DocumentId documentId, ImmutableArray<DiagnosticData> items)
private void RaiseDiagnosticsCreated(object id, Solution solution, ProjectId projectId, DocumentId documentId, ImmutableArray<DiagnosticData> items)
{
DiagnosticsUpdated?.Invoke(this, DiagnosticsUpdatedArgs.DiagnosticsCreated(
CreateArgumentKey(id), _workspace, _workspace.CurrentSolution, projectId, documentId, items));
CreateArgumentKey(id), _workspace, solution, projectId, documentId, items));
}
private void RaiseDiagnosticsRemoved(object id, ProjectId projectId, DocumentId documentId)
private void RaiseDiagnosticsRemoved(object id, Solution solution, ProjectId projectId, DocumentId documentId)
{
DiagnosticsUpdated?.Invoke(this, DiagnosticsUpdatedArgs.DiagnosticsRemoved(
CreateArgumentKey(id), _workspace, _workspace.CurrentSolution, projectId, documentId));
CreateArgumentKey(id), _workspace, solution, projectId, documentId));
}
private static ArgumentKey CreateArgumentKey(object id) => new ArgumentKey(id);
......@@ -392,14 +387,17 @@ private void RaiseBuildStarted(bool started)
private class InprogressState
{
private readonly ExternalErrorDiagnosticUpdateSource _owner;
private readonly Solution _solution;
private readonly HashSet<ProjectId> _builtProjects = new HashSet<ProjectId>();
private readonly Dictionary<ProjectId, HashSet<DiagnosticData>> _projectMap = new Dictionary<ProjectId, HashSet<DiagnosticData>>();
private readonly Dictionary<DocumentId, HashSet<DiagnosticData>> _documentMap = new Dictionary<DocumentId, HashSet<DiagnosticData>>();
private readonly Dictionary<ProjectId, HashSet<string>> _diagnosticIdMap = new Dictionary<ProjectId, HashSet<string>>();
public InprogressState(ExternalErrorDiagnosticUpdateSource owner)
public InprogressState(ExternalErrorDiagnosticUpdateSource owner, Solution solution)
{
_owner = owner;
_solution = solution;
// let people know build has started
// TODO: to be more accurate, it probably needs to be counted. but for now,
......@@ -407,11 +405,38 @@ public InprogressState(ExternalErrorDiagnosticUpdateSource owner)
_owner.RaiseBuildStarted(started: true);
}
public Solution Solution => _solution;
public void Done()
{
_owner.RaiseBuildStarted(started: false);
}
public bool SupportedDiagnosticId(ProjectId projectId, string id)
{
HashSet<string> ids;
if (_diagnosticIdMap.TryGetValue(projectId, out ids))
{
return ids.Contains(id);
}
// set ids set
var map = new HashSet<string>();
_diagnosticIdMap.Add(projectId, map);
var project = _solution.GetProject(projectId);
if (project == null)
{
// projectId no longer exist, return false;
return false;
}
var descriptorMap = _owner._diagnosticService.GetDiagnosticDescriptors(project);
map.UnionWith(descriptorMap.Values.SelectMany(v => v.Select(d => d.Id)));
return map.Contains(id);
}
public ImmutableArray<DiagnosticData> GetBuildDiagnostics()
{
return ImmutableArray.CreateRange(_projectMap.Values.SelectMany(d => d).Concat(_documentMap.Values.SelectMany(d => d)));
......@@ -467,6 +492,11 @@ public void AddError(DocumentId key, DiagnosticData diagnostic)
AddError(_documentMap, key, diagnostic);
}
public void AddError(ProjectId key, DiagnosticData diagnostic)
{
AddError(_projectMap, key, diagnostic);
}
private void AddErrors<T>(Dictionary<T, HashSet<DiagnosticData>> map, T key, HashSet<DiagnosticData> diagnostics)
{
var errors = GetErrorSet(map, key);
......
......@@ -24,6 +24,7 @@ internal class ProjectExternalErrorReporter : IVsReportExternalErrors, IVsLangua
private readonly ProjectId _projectId;
private readonly string _errorCodePrefix;
private readonly VisualStudioWorkspaceImpl _workspace;
private readonly ExternalErrorDiagnosticUpdateSource _diagnosticProvider;
......@@ -31,11 +32,23 @@ public ProjectExternalErrorReporter(ProjectId projectId, string errorCodePrefix,
{
_projectId = projectId;
_errorCodePrefix = errorCodePrefix;
_diagnosticProvider = serviceProvider.GetMefService<ExternalErrorDiagnosticUpdateSource>();
_workspace = serviceProvider.GetMefService<VisualStudioWorkspaceImpl>();
_diagnosticProvider = serviceProvider.GetMefService<ExternalErrorDiagnosticUpdateSource>();
Debug.Assert(_diagnosticProvider != null);
Debug.Assert(_workspace != null);
Debug.Assert(_diagnosticProvider != null);
}
private bool CanHandle(string errorId)
{
// we accept all compiler diagnostics
if (errorId.StartsWith(_errorCodePrefix))
{
return true;
}
return _diagnosticProvider.SupportedDiagnosticId(_projectId, errorId);
}
public int AddNewErrors(IVsEnumExternalErrors pErrors)
......@@ -134,28 +147,20 @@ public int ReportError(string bstrErrorMessage, string bstrErrorId, [ComAliasNam
// TODO: Use PreserveSig instead of throwing these exceptions for common cases.
public void ReportError2(string bstrErrorMessage, string bstrErrorId, [ComAliasName("VsShell.VSTASKPRIORITY")]VSTASKPRIORITY nPriority, int iStartLine, int iStartColumn, int iEndLine, int iEndColumn, string bstrFileName)
{
if ((iEndLine >= 0 && iEndColumn >= 0) &&
((iEndLine < iStartLine) ||
(iEndLine == iStartLine && iEndColumn < iStartColumn)))
{
throw new ArgumentException(ServicesVSResources.EndPositionMustBeGreaterThanStart);
}
// We only handle errors that have positions. For the rest, we punt back to the
// project system.
if (iStartLine < 0 || iStartColumn < 0)
// first we check whether given error is something we can take care.
if (!CanHandle(bstrErrorId))
{
// it is not, let project system takes care.
throw new NotImplementedException();
}
var hostProject = _workspace.GetHostProject(_projectId);
if (!hostProject.ContainsFile(bstrFileName))
if ((iEndLine >= 0 && iEndColumn >= 0) &&
((iEndLine < iStartLine) ||
(iEndLine == iStartLine && iEndColumn < iStartColumn)))
{
throw new NotImplementedException();
throw new ArgumentException(ServicesVSResources.EndPositionMustBeGreaterThanStart);
}
var hostDocument = hostProject.GetCurrentDocumentFromPath(bstrFileName);
var priority = (VSTASKPRIORITY)nPriority;
DiagnosticSeverity severity;
switch (priority)
......@@ -173,6 +178,32 @@ public void ReportError2(string bstrErrorMessage, string bstrErrorId, [ComAliasN
throw new ArgumentException(ServicesVSResources.NotAValidValue, nameof(nPriority));
}
if (iStartLine < 0 || iStartColumn < 0)
{
// we now takes care of errors that is not belong to file as well.
var projectDiagnostic = GetDiagnosticData(
null, bstrErrorId, bstrErrorMessage, severity,
null, 0, 0, 0, 0,
bstrFileName, 0, 0, 0, 0);
_diagnosticProvider.AddNewErrors(_projectId, projectDiagnostic);
return;
}
var hostProject = _workspace.GetHostProject(_projectId);
if (!hostProject.ContainsFile(bstrFileName))
{
var projectDiagnostic = GetDiagnosticData(
null, bstrErrorId, bstrErrorMessage, severity,
null, iStartLine, iStartColumn, iEndLine, iEndColumn,
bstrFileName, iStartLine, iStartColumn, iEndLine, iEndColumn);
_diagnosticProvider.AddNewErrors(_projectId, projectDiagnostic);
return;
}
var hostDocument = hostProject.GetCurrentDocumentFromPath(bstrFileName);
var diagnostic = GetDiagnosticData(
hostDocument.Id, bstrErrorId, bstrErrorMessage, severity,
null, iStartLine, iStartColumn, iEndLine, iEndColumn,
......
......@@ -53,6 +53,21 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
End Using
End Function
<Fact>
Public Async Function TestExternalDiagnostics_SupportedId() As Task
Using workspace = Await TestWorkspace.CreateCSharpAsync(String.Empty)
Dim waiter = New Waiter()
Dim service = New TestDiagnosticAnalyzerService()
Dim source = New ExternalErrorDiagnosticUpdateSource(workspace, service, New MockDiagnosticUpdateSourceRegistrationService(), waiter)
Dim project = workspace.CurrentSolution.Projects.First()
source.OnSolutionBuild(Me, Shell.UIContextChangedEventArgs.From(True))
Assert.True(source.SupportedDiagnosticId(project.Id, "CS1002"))
Assert.False(source.SupportedDiagnosticId(project.Id, "CA1002"))
End Using
End Function
<Fact>
Public Async Function TestExternalDiagnostics_DuplicatedError() As Task
Using workspace = Await TestWorkspace.CreateCSharpAsync(String.Empty)
......@@ -169,7 +184,7 @@ Namespace Microsoft.VisualStudio.LanguageServices.UnitTests.Diagnostics
End Sub
Public Function GetDiagnosticDescriptors(projectOpt As Project) As ImmutableDictionary(Of String, ImmutableArray(Of DiagnosticDescriptor)) Implements IDiagnosticAnalyzerService.GetDiagnosticDescriptors
Return ImmutableDictionary(Of String, ImmutableArray(Of DiagnosticDescriptor)).Empty
Return ImmutableDictionary(Of String, ImmutableArray(Of DiagnosticDescriptor)).Empty.Add("reference", ImmutableArray.Create(Of DiagnosticDescriptor)(New DiagnosticDescriptor("CS1002", "test", "test", "test", DiagnosticSeverity.Warning, True)))
End Function
Public Function GetDiagnosticsForSpanAsync(document As Document, range As TextSpan, Optional includeSuppressedDiagnostics As Boolean = False, Optional cancellationToken As CancellationToken = Nothing) As Task(Of IEnumerable(Of DiagnosticData)) Implements IDiagnosticAnalyzerService.GetDiagnosticsForSpanAsync
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册