diff --git a/docs/features/local-functions.md b/docs/features/local-functions.md index 545eb88a735ae48d135afc102122c68604bd0711..8b45585648f47b163b4d57c1af049cb0c2d8d9ce 100644 --- a/docs/features/local-functions.md +++ b/docs/features/local-functions.md @@ -12,28 +12,29 @@ Syntax Grammar This grammar is represented as a diff from the current spec grammar. ```diff -declaration-statement - : local-variable-declaration ';' - | local-constant-declaration ';' -+ | local-function-declaration + declaration_statement + : local_variable_declaration ';' + | local_constant_declaration ';' ++ | local_function_declaration ; -+local-function-declaration -+ : local-function-header local-function-body ++local_function_declaration ++ : local_function_header local_function_body + ; -+local-function-header -+ : local-function-modifiers? return-type identifier type-parameter-list? -+ ( formal-parameter-list? ) type-parameter-constraints-clauses ++local_function_header ++ : local_function_modifier* return_type identifier type_parameter_list? ++ '(' formal_parameter_list? ')' type_parameter_constraints_clauses + ; -+local-function-modifiers -+ : (async | unsafe) ++local_function_modifier ++ : 'async' ++ | 'unsafe' + ; -+local-function-body ++local_function_body + : block -+ | arrow-expression-body ++ | arrow_expression_body + ; ``` diff --git a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs index bd436b71a7ac9eba7f884a8d90c13f94024eadb0..e48a20a85e881a511600c2a3d6ef6950437197ac 100644 --- a/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs +++ b/src/Compilers/CSharp/Test/Semantic/Diagnostics/DiagnosticAnalyzerTests.cs @@ -916,6 +916,31 @@ public void TestReportingDiagnosticWithInvalidLocation() } } + [Fact] + public void TestReportingDiagnosticWithInvalidSpan() + { + var source1 = @"class C1 { void M() { int i = 0; i++; } }"; + var compilation = CreateCompilationWithMscorlib45(source1, parseOptions: TestOptions.RegularWithIOperationFeature); + var treeInAnotherCompilation = compilation.SyntaxTrees.Single(); + + var badSpan = new Text.TextSpan(100000, 10000); + + string message = new ArgumentException( + string.Format(CodeAnalysisResources.InvalidDiagnosticSpanReported, AnalyzerWithInvalidDiagnosticSpan.Descriptor.Id, badSpan, treeInAnotherCompilation.FilePath), "diagnostic").Message; + + compilation.VerifyDiagnostics(); + + var analyzer = new AnalyzerWithInvalidDiagnosticSpan(badSpan); + var analyzers = new DiagnosticAnalyzer[] { analyzer }; + compilation + .VerifyAnalyzerDiagnostics(analyzers, null, null, logAnalyzerExceptionAsDiagnostics: true, + expected: + Diagnostic("AD0001") + .WithArguments("Microsoft.CodeAnalysis.CommonDiagnosticAnalyzers+AnalyzerWithInvalidDiagnosticSpan", "System.ArgumentException", message) + .WithLocation(1, 1) + ); + } + [Fact, WorkItem(13120, "https://github.com/dotnet/roslyn/issues/13120")] public void TestRegisteringAsyncAnalyzerMethod() { @@ -1903,7 +1928,7 @@ public class RegularClass var tree = CSharpSyntaxTree.ParseText(source, path: "Source.cs"); var compilation = CreateCompilationWithMscorlib45(new[] { tree }); compilation.VerifyDiagnostics(); - + var analyzers = new DiagnosticAnalyzer[] { new GeneratedCodeAnalyzer(GeneratedCodeAnalysisFlags.None) }; compilation.VerifyAnalyzerDiagnostics(analyzers, null, null, true, Diagnostic("GeneratedCodeAnalyzerWarning", "}").WithArguments("Source.cs").WithLocation(11, 1), diff --git a/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs b/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs index 128f8df04c48814c2da5d392e424f83d7c5df843..be93d74078b7f062c15fa52d77311098b08fc368 100644 --- a/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs +++ b/src/Compilers/Core/Portable/CodeAnalysisResources.Designer.cs @@ -20,7 +20,7 @@ namespace Microsoft.CodeAnalysis { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CodeAnalysisResources { @@ -748,6 +748,15 @@ internal class CodeAnalysisResources { } } + /// + /// Looks up a localized string similar to Reported diagnostic '{0}' has a source location '{1}' in file '{2}', which is outside of the given file.. + /// + internal static string InvalidDiagnosticSpanReported { + get { + return ResourceManager.GetString("InvalidDiagnosticSpanReported", resourceCulture); + } + } + /// /// Looks up a localized string similar to Invalid hash.. /// @@ -1334,7 +1343,7 @@ internal class CodeAnalysisResources { } /// - /// Looks up a localized string similar to Windows PDB writer doesn't support deterministic compilation: '{0}'. + /// Looks up a localized string similar to Windows PDB writer doesn't support SourceLink feature: '{0}'. /// internal static string SymWriterDoesNotSupportSourceLink { get { @@ -1343,7 +1352,7 @@ internal class CodeAnalysisResources { } /// - /// Looks up a localized string similar to Windows PDB writer is not available -- could not find Microsoft.DiaSymReader.Native.{0}.dll. + /// Looks up a localized string similar to Windows PDB writer doesn't support deterministic compilation: '{0}'. /// internal static string SymWriterNotDeterministic { get { diff --git a/src/Compilers/Core/Portable/CodeAnalysisResources.resx b/src/Compilers/Core/Portable/CodeAnalysisResources.resx index 17dde4a37fd63fb92561c6e0cf33c05fae6ab159..102db4f2b47edc27a8b3c8a21676fece1f940961 100644 --- a/src/Compilers/Core/Portable/CodeAnalysisResources.resx +++ b/src/Compilers/Core/Portable/CodeAnalysisResources.resx @@ -612,4 +612,7 @@ Stream contains invalid data + + Reported diagnostic '{0}' has a source location '{1}' in file '{2}', which is outside of the given file. + \ No newline at end of file diff --git a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContextHelpers.cs b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContextHelpers.cs index 4db78d5c4873e8ccccdedcb7cf01002659159525..58cf39ca3a59b8347fbc7dddde93508aa8fe00b0 100644 --- a/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContextHelpers.cs +++ b/src/Compilers/Core/Portable/DiagnosticAnalyzer/DiagnosticAnalysisContextHelpers.cs @@ -73,7 +73,7 @@ internal static void VerifyDiagnosticLocationsInCompilation(Diagnostic diagnosti } } } - + internal static void VerifyIOperationFeatureFlag(bool isIOperationFeatureEnabled) { if (!isIOperationFeatureEnabled) @@ -84,11 +84,22 @@ internal static void VerifyIOperationFeatureFlag(bool isIOperationFeatureEnabled private static void VerifyDiagnosticLocationInCompilation(string id, Location location, Compilation compilation) { - if (location.IsInSource && !compilation.ContainsSyntaxTree(location.SourceTree)) + if (!location.IsInSource) + { + return; + } + + if (!compilation.ContainsSyntaxTree(location.SourceTree)) { // Disallow diagnostics with source locations outside this compilation. throw new ArgumentException(string.Format(CodeAnalysisResources.InvalidDiagnosticLocationReported, id, location.SourceTree.FilePath), "diagnostic"); } + + if (location.SourceSpan.End > location.SourceTree.Length) + { + // Disallow diagnostics with source locations outside this compilation. + throw new ArgumentException(string.Format(CodeAnalysisResources.InvalidDiagnosticSpanReported, id, location.SourceSpan, location.SourceTree.FilePath), "diagnostic"); + } } private static void VerifyAction(Action action) diff --git a/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs b/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs index eb20ae761fa3ed5eaee8299869dded6731c89700..658b8cfe2c3020602d2dd120773a3aa4c46ea321 100644 --- a/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs +++ b/src/EditorFeatures/CSharpTest/Diagnostics/DiagnosticAnalyzerDriver/DiagnosticAnalyzerDriverTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Test.Utilities; @@ -312,5 +313,34 @@ public void AnalyzeNode(SyntaxNodeAnalysisContext context) } } } + + [Fact] + public async Task TestDiagnosticSpan() + { + var source = @"// empty code"; + + var analyzer = new InvalidSpanAnalyzer(); + using (var compilerEngineWorkspace = TestWorkspace.CreateCSharp(source)) + { + var compilerEngineCompilation = (CSharpCompilation)(await compilerEngineWorkspace.CurrentSolution.Projects.Single().GetCompilationAsync()); + + var diagnostics = compilerEngineCompilation.GetAnalyzerDiagnostics(new[] { analyzer }); + AssertEx.Any(diagnostics, d => d.Id == AnalyzerHelper.AnalyzerExceptionDiagnosticId); + } + } + + private class InvalidSpanAnalyzer : DiagnosticAnalyzer + { + public static DiagnosticDescriptor Descriptor = DescriptorFactory.CreateSimpleDescriptor("DummyDiagnostic"); + + public override ImmutableArray SupportedDiagnostics + => ImmutableArray.Create(Descriptor); + + public override void Initialize(AnalysisContext context) + => context.RegisterSyntaxTreeAction(Analyze); + + private void Analyze(SyntaxTreeAnalysisContext context) + => context.ReportDiagnostic(Diagnostic.Create(Descriptor, Location.Create(context.Tree, TextSpan.FromBounds(1000, 2000)))); + } } } diff --git a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs index 047ff9592b0159bb3c7da9a3aac07f62315bdbdb..d3879ced7d0fc1f9f3f45e7de82e33fb74bb8ed1 100644 --- a/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs +++ b/src/EditorFeatures/Core/Implementation/Diagnostics/AbstractDiagnosticsTaggerProvider.TaggerProvider.cs @@ -8,6 +8,7 @@ using Microsoft.CodeAnalysis.Editor.Shared.Preview; using Microsoft.CodeAnalysis.Editor.Shared.Tagging; using Microsoft.CodeAnalysis.Editor.Tagging; +using Microsoft.CodeAnalysis.ErrorReporting; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Text; using Microsoft.CodeAnalysis.Text.Shared.Extensions; @@ -63,7 +64,7 @@ protected override ITaggerEventSource CreateEventSource(ITextView textViewOpt, I // We act as a source of events ourselves. When the diagnostics service tells // us about new diagnostics, we'll use that to kick of the asynchronous tagging // work. - + var eventSource = TaggerEventSources.Compose( TaggerEventSources.OnWorkspaceRegistrationChanged(subjectBuffer, TaggerDelay.Medium), this); @@ -83,69 +84,79 @@ protected override Task ProduceTagsAsync(TaggerContext context, DocumentSn private void ProduceTags(TaggerContext context, DocumentSnapshotSpan spanToTag) { - if (!_owner.IsEnabled) + try { - return; - } + if (!_owner.IsEnabled) + { + return; + } - var document = spanToTag.Document; - if (document == null) - { - return; - } + var document = spanToTag.Document; + if (document == null) + { + return; + } - // See if we've marked any spans as those we want to suppress diagnostics for. - // This can happen for buffers used in the preview workspace where some feature - // is generating code that it doesn't want errors shown for. - var buffer = spanToTag.SnapshotSpan.Snapshot.TextBuffer; - var suppressedDiagnosticsSpans = default(NormalizedSnapshotSpanCollection); - buffer?.Properties.TryGetProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, out suppressedDiagnosticsSpans); - - // Producing tags is simple. We just grab the diagnostics we were already told about, - // and we convert them to tag spans. - object id; - ImmutableArray diagnostics; - SourceText sourceText; - ITextSnapshot editorSnapshot; - lock (_gate) - { - id = _latestId; - diagnostics = _latestDiagnostics; - sourceText = _latestSourceText; - editorSnapshot = _latestEditorSnapshot; - } + // See if we've marked any spans as those we want to suppress diagnostics for. + // This can happen for buffers used in the preview workspace where some feature + // is generating code that it doesn't want errors shown for. + var buffer = spanToTag.SnapshotSpan.Snapshot.TextBuffer; + var suppressedDiagnosticsSpans = default(NormalizedSnapshotSpanCollection); + buffer?.Properties.TryGetProperty(PredefinedPreviewTaggerKeys.SuppressDiagnosticsSpansKey, out suppressedDiagnosticsSpans); + + // Producing tags is simple. We just grab the diagnostics we were already told about, + // and we convert them to tag spans. + object id; + ImmutableArray diagnostics; + SourceText sourceText; + ITextSnapshot editorSnapshot; + lock (_gate) + { + id = _latestId; + diagnostics = _latestDiagnostics; + sourceText = _latestSourceText; + editorSnapshot = _latestEditorSnapshot; + } - if (sourceText == null || editorSnapshot == null) - { - return; - } + if (sourceText == null || editorSnapshot == null) + { + return; + } - var project = document.Project; + var project = document.Project; - var requestedSnapshot = spanToTag.SnapshotSpan.Snapshot; - var requestedSpan = spanToTag.SnapshotSpan; - var isLiveUpdate = id is ISupportLiveUpdate; + var requestedSnapshot = spanToTag.SnapshotSpan.Snapshot; + var requestedSpan = spanToTag.SnapshotSpan; + var isLiveUpdate = id is ISupportLiveUpdate; - foreach (var diagnosticData in diagnostics) - { - if (_owner.IncludeDiagnostic(diagnosticData)) + foreach (var diagnosticData in diagnostics) { - var actualSpan = diagnosticData - .GetExistingOrCalculatedTextSpan(sourceText) - .ToSnapshotSpan(editorSnapshot) - .TranslateTo(requestedSnapshot, SpanTrackingMode.EdgeExclusive); - - if (actualSpan.IntersectsWith(requestedSpan) && - !IsSuppressed(suppressedDiagnosticsSpans, actualSpan)) + if (_owner.IncludeDiagnostic(diagnosticData)) { - var tagSpan = _owner.CreateTagSpan(isLiveUpdate, actualSpan, diagnosticData); - if (tagSpan != null) + var actualSpan = diagnosticData + .GetExistingOrCalculatedTextSpan(sourceText) + .ToSnapshotSpan(editorSnapshot) + .TranslateTo(requestedSnapshot, SpanTrackingMode.EdgeExclusive); + + if (actualSpan.IntersectsWith(requestedSpan) && + !IsSuppressed(suppressedDiagnosticsSpans, actualSpan)) { - context.AddTag(tagSpan); + var tagSpan = _owner.CreateTagSpan(isLiveUpdate, actualSpan, diagnosticData); + if (tagSpan != null) + { + context.AddTag(tagSpan); + } } } } } + catch (ArgumentOutOfRangeException ex) when (FatalError.ReportWithoutCrash(ex)) + { + // https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_workitems?id=428328&_a=edit&triage=false + // explicitly report NFW to find out what is causing us for out of range. + // stop crashing on such occations + return; + } } private bool IsSuppressed(NormalizedSnapshotSpanCollection suppressedSpans, SnapshotSpan span) diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs index 0cef847a5ceff3eeda020fbb42fe5fe40e738fb3..ec54350ba0dbeb32627d97bba1aebe0d776e5523 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.Executor.cs @@ -377,7 +377,7 @@ public IEnumerable ConvertToLocalDiagnostics(Document targetDocu try { - var diagnostics = await analyzer.AnalyzeProjectAsync(project, cancellationToken).ConfigureAwait(false); + var diagnostics = (await analyzer.AnalyzeProjectAsync(project, cancellationToken).ConfigureAwait(false)).NullToEmpty(); // Apply filtering from compilation options (source suppressions, ruleset, etc.) if (compilationOpt != null) @@ -385,6 +385,13 @@ public IEnumerable ConvertToLocalDiagnostics(Document targetDocu diagnostics = CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationOpt).ToImmutableArrayOrEmpty(); } +#if DEBUG + // since all ProjectDiagnosticAnalyzers are from internal users, we only do debug check. also this can be expensive at runtime + // since it requires await. if we find any offender through NFW, we should be able to fix those since all those should + // from intern teams. + await VerifyDiagnosticLocationsAsync(diagnostics, project, cancellationToken).ConfigureAwait(false); +#endif + return diagnostics; } catch (Exception e) when (!IsCanceled(e, cancellationToken)) @@ -423,6 +430,13 @@ public IEnumerable ConvertToLocalDiagnostics(Document targetDocu return CompilationWithAnalyzers.GetEffectiveDiagnostics(diagnostics, compilationOpt); } +#if DEBUG + // since all DocumentDiagnosticAnalyzers are from internal users, we only do debug check. also this can be expensive at runtime + // since it requires await. if we find any offender through NFW, we should be able to fix those since all those should + // from intern teams. + await VerifyDiagnosticLocationsAsync(diagnostics, document.Project, cancellationToken).ConfigureAwait(false); +#endif + return diagnostics; } catch (Exception e) when (!IsCanceled(e, cancellationToken)) @@ -584,6 +598,86 @@ private IEnumerable ConvertToLocalDiagnosticsWithCompilation(Doc } } + private static async Task VerifyDiagnosticLocationsAsync(ImmutableArray diagnostics, Project project, CancellationToken cancellationToken) + { + foreach (var diagnostic in diagnostics) + { + await VerifyDiagnosticLocationAsync(diagnostic.Id, diagnostic.Location, project, cancellationToken).ConfigureAwait(false); + + if (diagnostic.AdditionalLocations != null) + { + foreach (var location in diagnostic.AdditionalLocations) + { + await VerifyDiagnosticLocationAsync(diagnostic.Id, diagnostic.Location, project, cancellationToken).ConfigureAwait(false); + } + } + } + } + + private static async Task VerifyDiagnosticLocationAsync(string id, Location location, Project project, CancellationToken cancellationToken) + { + switch (location.Kind) + { + case LocationKind.None: + case LocationKind.MetadataFile: + case LocationKind.XmlFile: + // ignore these kinds + break; + case LocationKind.SourceFile: + { + if (project.GetDocument(location.SourceTree) == null) + { + // Disallow diagnostics with source locations outside this project. + throw new ArgumentException(string.Format(FeaturesResources.Reported_diagnostic_0_has_a_source_location_in_file_1_which_is_not_part_of_the_compilation_being_analyzed, id, location.SourceTree.FilePath), "diagnostic"); + } + + if (location.SourceSpan.End > location.SourceTree.Length) + { + // Disallow diagnostics with source locations outside this project. + throw new ArgumentException(string.Format(FeaturesResources.Reported_diagnostic_0_has_a_source_location_1_in_file_2_which_is_outside_of_the_given_file, id, location.SourceSpan, location.SourceTree.FilePath), "diagnostic"); + } + } + break; + case LocationKind.ExternalFile: + { + var filePath = location.GetLineSpan().Path; + var document = TryGetDocumentWithFilePath(project, filePath); + if (document == null) + { + // this is not a roslyn file. we don't care about this file. + return; + } + + // this can be potentially expensive since it will load text if it is not already loaded. + // but, this text is most likely already loaded since producer of this diagnostic (Document/ProjectDiagnosticAnalyzers) + // should have loaded it to produce the diagnostic at the first place. once loaded, it should stay in memory until + // project cache goes away. when text is already there, await should return right away. + var text = await document.GetTextAsync(cancellationToken).ConfigureAwait(false); + if (location.SourceSpan.End > text.Length) + { + // Disallow diagnostics with locations outside this project. + throw new ArgumentException(string.Format(FeaturesResources.Reported_diagnostic_0_has_a_source_location_1_in_file_2_which_is_outside_of_the_given_file, id, location.SourceSpan, filePath), "diagnostic"); + } + } + break; + default: + throw ExceptionUtilities.Unreachable; + } + } + + private static Document TryGetDocumentWithFilePath(Project project, string path) + { + foreach (var documentId in project.Solution.GetDocumentIdsWithFilePath(path)) + { + if (documentId.ProjectId == project.Id) + { + return project.GetDocument(documentId); + } + } + + return null; + } + private static void GetLogFunctionIdAndTitle(AnalysisKind kind, out FunctionId functionId, out string title) { switch (kind) diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs index 0f4939afaed8c753f85dd3865f6fd1f9d42b52d1..0d5a25f899e0d2644be1493d11ed80801444d625 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer.ProjectState.cs @@ -225,7 +225,7 @@ public async Task SaveAsync(Project project, DiagnosticAnalysisResult result) await SerializeAsync(serializer, project, result.ProjectId, _owner.NonLocalStateName, result.Others).ConfigureAwait(false); - AnalyzerABTestLogger.LogProjectDiagnostics(project, result); + AnalyzerABTestLogger.LogProjectDiagnostics(project, _owner.StateName, result); } public void ResetVersion() @@ -244,7 +244,7 @@ public async Task MergeAsync(ActiveFileState state, Document document) var syntax = state.GetAnalysisData(AnalysisKind.Syntax); var semantic = state.GetAnalysisData(AnalysisKind.Semantic); - AnalyzerABTestLogger.LogDocumentDiagnostics(document, syntax.Items, semantic.Items); + AnalyzerABTestLogger.LogDocumentDiagnostics(document, _owner.StateName, syntax.Items, semantic.Items); var project = document.Project; diff --git a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs index bd7260fbfb231db5ef2f23c3603dc725e5543069..30d10234e191afb15bbcb1c9dffdb05f5f69cdfd 100644 --- a/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs +++ b/src/Features/Core/Portable/Diagnostics/EngineV2/DiagnosticIncrementalAnalyzer_BuildSynchronization.cs @@ -1,7 +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. +using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Globalization; using System.Linq; using System.Threading; @@ -15,6 +17,8 @@ internal partial class DiagnosticIncrementalAnalyzer { public async Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictionary> map) { + DebugVerifyDiagnosticLocations(map); + if (!PreferBuildErrors(workspace)) { // prefer live errors over build errors @@ -51,6 +55,18 @@ public async Task SynchronizeWithBuildAsync(Workspace workspace, ImmutableDictio } } + [Conditional("DEBUG")] + private void DebugVerifyDiagnosticLocations(ImmutableDictionary> map) + { + foreach (var diagnostic in map.Values.SelectMany(v => v)) + { + // errors from build shouldn't have any span set. + // this is debug check since it gets data from us only not from third party unlike one in compiler + // that checks span for third party reported diagnostics + Contract.Requires(!diagnostic.HasTextSpan); + } + } + private async Task CreateProjectAnalysisDataAsync(Project project, ImmutableArray stateSets, ImmutableArray diagnostics) { // we always load data sicne we don't know right version. diff --git a/src/Features/Core/Portable/Experimentation/AnalyzerABTestLogger.cs b/src/Features/Core/Portable/Experimentation/AnalyzerABTestLogger.cs index fd2f932d98ccd4e19afea08dcb1e153f4b0dabd9..c93696fdf008bfad1c16e7642988fb7df7d776fb 100644 --- a/src/Features/Core/Portable/Experimentation/AnalyzerABTestLogger.cs +++ b/src/Features/Core/Portable/Experimentation/AnalyzerABTestLogger.cs @@ -16,7 +16,7 @@ namespace Microsoft.CodeAnalysis.Experimentation internal static class AnalyzerABTestLogger { private static bool s_reportErrors = false; - private static readonly ConcurrentDictionary s_reported = new ConcurrentDictionary(concurrencyLevel: 2, capacity: 10); + private static readonly ConcurrentDictionary<(object, string), object> s_reported = new ConcurrentDictionary<(object, string), object>(concurrencyLevel: 2, capacity: 10); private const string Name = "LiveCodeAnalysisVsix"; @@ -59,9 +59,9 @@ public static void LogCandidacyRequirementsTracking(long lastTriggeredTimeBinary } } - public static void LogProjectDiagnostics(Project project, DiagnosticAnalysisResult result) + public static void LogProjectDiagnostics(Project project, string analyzerName, DiagnosticAnalysisResult result) { - if (!s_reportErrors || !s_reported.TryAdd(project.Id, null)) + if (!s_reportErrors || !s_reported.TryAdd((project.Id, analyzerName), null)) { // doesn't meet the bar to report the issue. return; @@ -82,9 +82,9 @@ public static void LogProjectDiagnostics(Project project, DiagnosticAnalysisResu LogErrors(project, "ProjectDignostics", project.Id.Id, map); } - public static void LogDocumentDiagnostics(Document document, ImmutableArray syntax, ImmutableArray semantic) + public static void LogDocumentDiagnostics(Document document, string analyzerName, ImmutableArray syntax, ImmutableArray semantic) { - if (!s_reportErrors || !s_reported.TryAdd(document.Id, null)) + if (!s_reportErrors || !s_reported.TryAdd((document.Id, analyzerName), null)) { // doesn't meet the bar to report the issue. return; diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index ec4e580b883516927e732c8d34ee88c0c80c99f3..5505e8518aa6928d4550b97e6c1d3910fa4b27fd 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -2583,6 +2583,26 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Reported diagnostic '{0}' has a source location '{1}' in file '{2}', which is outside of the given file.. + /// + internal static string Reported_diagnostic_0_has_a_source_location_1_in_file_2_which_is_outside_of_the_given_file { + get { + return ResourceManager.GetString("Reported_diagnostic_0_has_a_source_location_1_in_file_2_which_is_outside_of_the_g" + + "iven_file", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reported diagnostic '{0}' has a source location in file '{1}', which is not part of the compilation being analyzed.. + /// + internal static string Reported_diagnostic_0_has_a_source_location_in_file_1_which_is_not_part_of_the_compilation_being_analyzed { + get { + return ResourceManager.GetString("Reported_diagnostic_0_has_a_source_location_in_file_1_which_is_not_part_of_the_co" + + "mpilation_being_analyzed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Reported diagnostic with ID '{0}' is not supported by the analyzer.. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 7e20e836b30ef4bc8b8c1871a86db0812f91f8cc..d00dccb2c347f711654adc4f65fa4418fc466272 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1247,6 +1247,12 @@ This version used in: {2} 'default' expression can be simplified + + Reported diagnostic '{0}' has a source location in file '{1}', which is not part of the compilation being analyzed. + + + Reported diagnostic '{0}' has a source location '{1}' in file '{2}', which is outside of the given file. + Unreachable code detected diff --git a/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs b/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs index 98d6ac34de454c4e74e69fd123e7c8999bf2d4d7..834224991e8a5d859ccf2918efc82045548eea36 100644 --- a/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs +++ b/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs @@ -460,6 +460,25 @@ public override void Initialize(AnalysisContext context) } } + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] + public sealed class AnalyzerWithInvalidDiagnosticSpan : DiagnosticAnalyzer + { + private readonly TextSpan _badSpan; + + public static readonly DiagnosticDescriptor Descriptor = new DiagnosticDescriptor( + "ID", + "Title1", + "Message", + "Category1", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public AnalyzerWithInvalidDiagnosticSpan(TextSpan badSpan) => _badSpan = badSpan; + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Descriptor); + public override void Initialize(AnalysisContext context) + => context.RegisterSyntaxTreeAction(c => c.ReportDiagnostic(Diagnostic.Create(Descriptor, SourceLocation.Create(c.Tree, _badSpan)))); + } + [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)] public sealed class AnalyzerWithInvalidDiagnosticLocation : DiagnosticAnalyzer {