diff --git a/docs/compilers/Error Log Format.md b/docs/compilers/Error Log Format.md index a873a78cb79696e529fd6be304be82646dd49120..baec297120759b7eec391c875e79972955ea467c 100644 --- a/docs/compilers/Error Log Format.md +++ b/docs/compilers/Error Log Format.md @@ -15,9 +15,9 @@ rather adds information that is specific to the implementation provided by the C# and Visual Basic Compilers. -Issue Properties +Result Properties ================ -The SARIF standard allows the `properties` property of `issue` objects +The SARIF standard allows the `properties` property of `result` objects to contain arbitrary (string, string) key-value pairs. The keys and values used by the C# and VB compilers are serialized from @@ -33,6 +33,4 @@ Key | Value "category" | `Diagnostic.Category` "helpLink" | `DiagnosticDescriptor.HelpLink` (omitted if null or empty) "isEnabledByDefault" | `Diagnostic.IsEnabledByDefault` ("True" or "False") -"isSuppressedInSource" | `Diagnostic.IsSuppressedInSource` ("True" or "False") -"customTags" | `Diagnostic.CustomTags` (joined together in a `;`-delimted list) "customProperties.[key]" | `Diagnostic.Properties[key]` (for each key in the dictionary) diff --git a/src/Compilers/CSharp/Test/CommandLine/ErrorLoggerTests.cs b/src/Compilers/CSharp/Test/CommandLine/ErrorLoggerTests.cs index 9602233ebac307ccbaf347e64ba5f003e2ccfe30..1aff8a586d50cb7f3fd488c31f05a8eb40e60ecc 100644 --- a/src/Compilers/CSharp/Test/CommandLine/ErrorLoggerTests.cs +++ b/src/Compilers/CSharp/Test/CommandLine/ErrorLoggerTests.cs @@ -46,7 +46,7 @@ public static void Main(string[] args) var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedIssues = @" - ""issues"": [ + ""results"": [ ] } ] @@ -57,7 +57,7 @@ public static void Main(string[] args) CleanupAllGeneratedFiles(hello); CleanupAllGeneratedFiles(errorLogFile); } - + [Fact] public void SimpleCompilerDiagnostics() { @@ -85,9 +85,10 @@ public class C var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedIssues = string.Format(@" - ""issues"": [ + ""results"": [ {{ ""ruleId"": ""CS0169"", + ""kind"": ""warning"", ""locations"": [ {{ ""analysisTarget"": [ @@ -104,29 +105,37 @@ public class C }} ], ""fullMessage"": ""The field 'C.x' is never used"", + ""isSuppressedInSource"": false, + ""tags"": [ + ""Compiler"", + ""Telemetry"" + ], ""properties"": {{ ""severity"": ""Warning"", ""warningLevel"": ""3"", ""defaultSeverity"": ""Warning"", ""title"": ""Field is never used"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""False"", - ""customTags"": ""Compiler;Telemetry"" + ""isEnabledByDefault"": ""True"" }} }}, {{ ""ruleId"": ""CS5001"", + ""kind"": ""error"", ""locations"": [ ], ""fullMessage"": ""Program does not contain a static 'Main' method suitable for an entry point"", + ""isSuppressedInSource"": false, + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ], ""properties"": {{ ""severity"": ""Error"", ""defaultSeverity"": ""Error"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""False"", - ""customTags"": ""Compiler;Telemetry;NotConfigurable"" + ""isEnabledByDefault"": ""True"" }} }} ] @@ -171,9 +180,10 @@ public class C var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedIssues = string.Format(@" - ""issues"": [ + ""results"": [ {{ ""ruleId"": ""CS0169"", + ""kind"": ""warning"", ""locations"": [ {{ ""analysisTarget"": [ @@ -190,29 +200,37 @@ public class C }} ], ""fullMessage"": ""The field 'C.x' is never used"", + ""isSuppressedInSource"": true, + ""tags"": [ + ""Compiler"", + ""Telemetry"" + ], ""properties"": {{ ""severity"": ""Warning"", ""warningLevel"": ""3"", ""defaultSeverity"": ""Warning"", ""title"": ""Field is never used"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""True"", - ""customTags"": ""Compiler;Telemetry"" + ""isEnabledByDefault"": ""True"" }} }}, {{ ""ruleId"": ""CS5001"", + ""kind"": ""error"", ""locations"": [ ], ""fullMessage"": ""Program does not contain a static 'Main' method suitable for an entry point"", + ""isSuppressedInSource"": false, + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ], ""properties"": {{ ""severity"": ""Error"", ""defaultSeverity"": ""Error"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""False"", - ""customTags"": ""Compiler;Telemetry;NotConfigurable"" + ""isEnabledByDefault"": ""True"" }} }} ] @@ -255,7 +273,7 @@ public class C var actualOutput = File.ReadAllText(errorLogFile).Trim(); var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); - var expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogIssuesText(cmd.Compilation); + var expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogResultsText(cmd.Compilation); var expectedText = expectedHeader + expectedIssues; Assert.Equal(expectedText, actualOutput); diff --git a/src/Compilers/Core/Portable/CodeAnalysis.csproj b/src/Compilers/Core/Portable/CodeAnalysis.csproj index f572bf5ef13a5a3dbb86e2d552920f6861dade94..33b7ca14830176adf91ca195b9bc1c1970dae752 100644 --- a/src/Compilers/Core/Portable/CodeAnalysis.csproj +++ b/src/Compilers/Core/Portable/CodeAnalysis.csproj @@ -70,6 +70,7 @@ + @@ -158,9 +159,6 @@ - - - diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index dd5fdd911b7567182bcb1bfc850ea92119a07071..a324fc5ffa1a1d004644064524a7f77750943ccb 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -34,7 +34,7 @@ internal abstract partial class CommonCompiler public abstract DiagnosticFormatter DiagnosticFormatter { get; } private readonly HashSet _reportedDiagnostics = new HashSet(); - public abstract Compilation CreateCompilation(TextWriter consoleOutput, TouchedFileLogger touchedFilesLogger, ErrorLogger errorLogger); + public abstract Compilation CreateCompilation(TextWriter consoleOutput, TouchedFileLogger touchedFilesLogger, ErrorLogger errorLoggerOpt); public abstract void PrintLogo(TextWriter consoleOutput); public abstract void PrintHelp(TextWriter consoleOutput); internal abstract string GetToolName(); @@ -177,7 +177,7 @@ internal static DiagnosticInfo ToFileReadDiagnostics(CommonMessageProvider messa return diagnosticInfo; } - public bool ReportErrors(IEnumerable diagnostics, TextWriter consoleOutput, ErrorLogger errorLogger) + public bool ReportErrors(IEnumerable diagnostics, TextWriter consoleOutput, ErrorLogger errorLoggerOpt) { bool hasErrors = false; foreach (var diag in diagnostics) @@ -202,7 +202,7 @@ public bool ReportErrors(IEnumerable diagnostics, TextWriter console // We want to report diagnostics with source suppression in the error log file. // However, these diagnostics should not be reported on the console output. - ErrorLogger.LogDiagnostic(diag, this.Culture, errorLogger); + errorLoggerOpt?.LogDiagnostic(diag, this.Culture); if (diag.IsSuppressed) { continue; @@ -221,7 +221,7 @@ public bool ReportErrors(IEnumerable diagnostics, TextWriter console return hasErrors; } - public bool ReportErrors(IEnumerable diagnostics, TextWriter consoleOutput, ErrorLogger errorLogger) + public bool ReportErrors(IEnumerable diagnostics, TextWriter consoleOutput, ErrorLogger errorLoggerOpt) { bool hasErrors = false; if (diagnostics != null && diagnostics.Any()) @@ -235,7 +235,7 @@ public bool ReportErrors(IEnumerable diagnostics, TextWriter con } PrintError(diagnostic, consoleOutput); - ErrorLogger.LogDiagnostic(Diagnostic.Create(diagnostic), this.Culture, errorLogger); + errorLoggerOpt?.LogDiagnostic(Diagnostic.Create(diagnostic), this.Culture); if (diagnostic.Severity == DiagnosticSeverity.Error) { diff --git a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.Issue.cs b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.Issue.cs deleted file mode 100644 index d9f12427990b00086f74f5e6aabf06af34af0442..0000000000000000000000000000000000000000 --- a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.Issue.cs +++ /dev/null @@ -1,58 +0,0 @@ -// 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.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; - -namespace Microsoft.CodeAnalysis -{ - internal partial class ErrorLogger - { - /// - /// Represents an issue to be logged into the error log. - /// This could be corresponding to a or a reported by the . - /// - private struct Issue - { - public readonly string Id; - public readonly string Message; - public readonly string Description; - public readonly string Title; - public readonly string Category; - public readonly string HelpLink; - public readonly bool IsEnabledByDefault; - public readonly bool IsSuppressedInSource; - public readonly DiagnosticSeverity DefaultSeverity; - public readonly DiagnosticSeverity Severity; - public readonly int WarningLevel; - public readonly Location Location; - public readonly IReadOnlyList AdditionalLocations; - public readonly IReadOnlyList CustomTags; - public readonly ImmutableArray> CustomProperties; - - public Issue( - string id, string message, string description, - string title, string category, string helpLink, bool isEnabledByDefault, bool isSuppressedInSource, - DiagnosticSeverity defaultSeverity, DiagnosticSeverity severity, int warningLevel, - Location location, IReadOnlyList additionalLocations, - IReadOnlyList customTags, ImmutableDictionary customProperties) - { - Id = id; - Message = message; - Description = description; - Title = title; - Category = category; - HelpLink = helpLink; - IsEnabledByDefault = isEnabledByDefault; - IsSuppressedInSource = isSuppressedInSource; - DefaultSeverity = defaultSeverity; - Severity = severity; - WarningLevel = warningLevel; - Location = location; - AdditionalLocations = additionalLocations; - CustomTags = customTags; - CustomProperties = customProperties.OrderBy(kvp => kvp.Key).ToImmutableArray(); - } - } - } -} diff --git a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.Value.cs b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.Value.cs deleted file mode 100644 index 5fa3776c557b6a8de8577a732a3e92caf5a32621..0000000000000000000000000000000000000000 --- a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.Value.cs +++ /dev/null @@ -1,128 +0,0 @@ -// 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.Collections.Generic; -using System.Collections.Immutable; - -namespace Microsoft.CodeAnalysis -{ - internal partial class ErrorLogger - { - /// - /// Represents a value for a key-value pair to be emitted into the error log file. - /// This could be a simple string or an integer OR could be a list of identical values OR a group of heterogeneous key-value pairs. - /// - private abstract class Value - { - protected readonly ErrorLogger Owner; - - protected Value(ErrorLogger owner) - { - Owner = owner; - } - - public static Value Create(string value, ErrorLogger owner) - { - return new StringValue(value, owner); - } - - public static Value Create(int value, ErrorLogger owner) - { - return new IntegerValue(value, owner); - } - - public static Value Create(ImmutableArray> values, ErrorLogger owner) - { - return new GroupValue(values, owner); - } - - public static Value Create(ImmutableArray values, ErrorLogger owner) - { - return new ListValue(values, owner); - } - - public abstract void Write(); - - private class StringValue : Value - { - private readonly string _value; - - public StringValue(string value, ErrorLogger owner) - : base(owner) - { - _value = value; - } - - public override void Write() - { - Owner.WriteValue(_value); - } - } - - private class IntegerValue : Value - { - private readonly int _value; - - public IntegerValue(int value, ErrorLogger owner) - : base(owner) - { - _value = value; - } - - public override void Write() - { - Owner.WriteValue(_value); - } - } - - private class GroupValue : Value - { - private readonly ImmutableArray> _keyValuePairs; - - public GroupValue(ImmutableArray> keyValuePairs, ErrorLogger owner) - : base(owner) - { - _keyValuePairs = keyValuePairs; - } - - public override void Write() - { - Owner.StartGroup(); - - bool isFirst = true; - foreach (var kvp in _keyValuePairs) - { - Owner.WriteKeyValuePair(kvp, isFirst); - isFirst = false; - } - - Owner.EndGroup(); - } - } - - private class ListValue : Value - { - private readonly ImmutableArray _values; - - public ListValue(ImmutableArray values, ErrorLogger owner) - : base(owner) - { - _values = values; - } - - public override void Write() - { - Owner.StartList(); - - bool isFirst = true; - foreach (var value in _values) - { - Owner.WriteValue(value, isFirst, valueInList: true); - isFirst = false; - } - - Owner.EndList(); - } - } - } - } -} diff --git a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.WellKnownStrings.cs b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.WellKnownStrings.cs deleted file mode 100644 index 245a894921b9172175980604399209e74b17727e..0000000000000000000000000000000000000000 --- a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.WellKnownStrings.cs +++ /dev/null @@ -1,47 +0,0 @@ -// 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.CodeAnalysis -{ - internal partial class ErrorLogger - { - /// - /// Contains well known property strings for error log file. - /// - private static class WellKnownStrings - { - public const string OutputFormatVersion = "version"; - public const string ToolFileVersion = "fileVersion"; - public const string ToolAssemblyVersion = "version"; - public const string ToolInfo = "toolInfo"; - public const string ToolName = "name"; - public const string RunLogs = "runLogs"; - public const string Issues = "issues"; - public const string DiagnosticId = "ruleId"; - public const string Locations = "locations"; - public const string ShortMessage = "shortMessage"; - public const string FullMessage = "fullMessage"; - public const string IsSuppressedInSource = "isSuppressedInSource"; - public const string Properties = "properties"; - public const string Location = "analysisTarget"; - public const string LocationSyntaxTreeUri = "uri"; - public const string LocationSpanInfo = "region"; - public const string LocationSpanStartLine = "startLine"; - public const string LocationSpanStartColumn = "startColumn"; - public const string LocationSpanEndLine = "endLine"; - public const string LocationSpanEndColumn = "endColumn"; - - // Diagnostic/DiagnosticDescriptor properties which are not defined in our log format. - public const string Category = "category"; - public const string Title = "title"; - public const string HelpLink = "helpLink"; - public const string CustomTags = "customTags"; - public const string IsEnabledByDefault = "isEnabledByDefault"; - public const string DefaultSeverity = "defaultSeverity"; - public const string Severity = "severity"; - public const string WarningLevel = "warningLevel"; - public const string CustomProperties = "customProperties"; - - public const string None = ""; - } - } -} diff --git a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs index 7e604d383f70a5d947928002be8eecc57318aa82..3632e8053cd7b5cc976f3b9fd80f640246f7d977 100644 --- a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs +++ b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs @@ -4,11 +4,12 @@ using System.IO; using System.Diagnostics; using System.Collections.Generic; -using System.Collections.Immutable; using System.Globalization; -using System.Runtime.Serialization.Json; +using System.Linq; using Roslyn.Utilities; +#pragma warning disable RS0013 // We need to invoke Diagnostic.Descriptor here to log all the metadata properties of the diagnostic. + namespace Microsoft.CodeAnalysis { /// @@ -20,154 +21,115 @@ namespace Microsoft.CodeAnalysis internal partial class ErrorLogger : IDisposable { // Internal for testing purposes. - internal const string OutputFormatVersion = "0.1"; - - private const string indentDelta = " "; - private const char groupStartChar = '{'; - private const char groupEndChar = '}'; - private const char listStartChar = '['; - private const char listEndChar = ']'; - - private readonly StreamWriter _writer; - private readonly DataContractJsonSerializer _jsonStringSerializer; + internal const string OutputFormatVersion = "0.4"; - private string _currentIndent; - private bool _reportedAnyIssues; + private readonly JsonWriter _writer; public ErrorLogger(Stream stream, string toolName, string toolFileVersion, Version toolAssemblyVersion) { Debug.Assert(stream != null); Debug.Assert(stream.Position == 0); - _writer = new StreamWriter(stream); - _jsonStringSerializer = new DataContractJsonSerializer(typeof(string)); - _currentIndent = string.Empty; - _reportedAnyIssues = false; + _writer = new JsonWriter(new StreamWriter(stream)); - WriteHeader(toolName, toolFileVersion, toolAssemblyVersion); - } - - private void WriteHeader(string toolName, string toolFileVersion, Version toolAssemblyVersion) - { - StartGroup(); + _writer.WriteObjectStart(); // root + _writer.Write("version", OutputFormatVersion); - WriteSimpleKeyValuePair(WellKnownStrings.OutputFormatVersion, OutputFormatVersion, isFirst: true); + _writer.WriteArrayStart("runLogs"); + _writer.WriteObjectStart(); // runLog - WriteKey(WellKnownStrings.RunLogs, isFirst: false); - StartList(); - StartNewEntry(isFirst: true); - StartGroup(); + WriteToolInfo(toolName, toolFileVersion, toolAssemblyVersion); - var toolInfo = GetToolInfo(toolName, toolFileVersion, toolAssemblyVersion); - WriteKeyValuePair(WellKnownStrings.ToolInfo, toolInfo, isFirst: true); - - WriteKey(WellKnownStrings.Issues, isFirst: false); - StartList(); + _writer.WriteArrayStart("results"); } - private Value GetToolInfo(string toolName, string toolFileVersion, Version toolAssemblyVersion) + private void WriteToolInfo(string name, string fileVersion, Version assemblyVersion) { - var builder = ArrayBuilder>.GetInstance(); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.ToolName, toolName)); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.ToolAssemblyVersion, toolAssemblyVersion.ToString(fieldCount: 3))); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.ToolFileVersion, toolFileVersion)); - return Value.Create(builder.ToImmutableAndFree(), this); + _writer.WriteObjectStart("toolInfo"); + _writer.Write("name", name); + _writer.Write("version", assemblyVersion.ToString(fieldCount: 3)); + _writer.Write("fileVersion", fileVersion); + _writer.WriteObjectEnd(); } - - internal static void LogDiagnostic(Diagnostic diagnostic, CultureInfo culture, ErrorLogger errorLogger) + internal void LogDiagnostic(Diagnostic diagnostic, CultureInfo culture) { - if (errorLogger != null) - { -#pragma warning disable RS0013 // We need to invoke Diagnostic.Descriptor here to log all the metadata properties of the diagnostic. - var issue = new Issue(diagnostic.Id, diagnostic.GetMessage(culture), - diagnostic.Descriptor.Description.ToString(culture), diagnostic.Descriptor.Title.ToString(culture), - diagnostic.Category, diagnostic.Descriptor.HelpLinkUri, diagnostic.IsEnabledByDefault, diagnostic.IsSuppressed, - diagnostic.DefaultSeverity, diagnostic.Severity, diagnostic.WarningLevel, diagnostic.Location, - diagnostic.AdditionalLocations, diagnostic.CustomTags, diagnostic.Properties); -#pragma warning restore RS0013 - - errorLogger.LogIssue(issue); - } - } + _writer.WriteObjectStart(); // result + _writer.Write("ruleId", diagnostic.Id); + _writer.Write("kind", GetKind(diagnostic.Severity)); - private void LogIssue(Issue issue) - { - var issueValue = GetIssueValue(issue); - WriteValue(issueValue, isFirst: !_reportedAnyIssues, valueInList: true); - _reportedAnyIssues = true; - } - - private Value GetIssueValue(Issue issue) - { - var builder = ArrayBuilder>.GetInstance(); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.DiagnosticId, issue.Id)); + WriteLocations(diagnostic.Location, diagnostic.AdditionalLocations); - var locationsValue = GetLocationsValue(issue.Location, issue.AdditionalLocations); - builder.Add(KeyValuePair.Create(WellKnownStrings.Locations, locationsValue)); + string message = diagnostic.GetMessage(culture); + if (string.IsNullOrEmpty(message)) + { + message = ""; + } - var message = string.IsNullOrEmpty(issue.Message) ? WellKnownStrings.None : issue.Message; - var description = issue.Description; + string description = diagnostic.Descriptor.Description.ToString(culture); if (string.IsNullOrEmpty(description)) { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.FullMessage, message)); + _writer.Write("fullMessage", message); } else { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.ShortMessage, message)); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.FullMessage, description)); + _writer.Write("shortMessage", message); + _writer.Write("fullMessage", description); } - var propertiesValue = GetPropertiesValue(issue); - builder.Add(KeyValuePair.Create(WellKnownStrings.Properties, propertiesValue)); + _writer.Write("isSuppressedInSource", diagnostic.IsSuppressed); - return Value.Create(builder.ToImmutableAndFree(), this); + WriteTags(diagnostic); + + WriteProperties(diagnostic, culture); + + _writer.WriteObjectEnd(); // result } - private Value GetLocationsValue(Location location, IReadOnlyList additionalLocations) + private void WriteLocations(Location location, IReadOnlyList additionalLocations) { - var builder = ArrayBuilder.GetInstance(); + _writer.WriteArrayStart("locations"); - var locationValue = GetLocationValue(location); - if (locationValue != null) - { - builder.Add(locationValue); - } + WriteLocation(location); - if (additionalLocations?.Count > 0) + if (additionalLocations != null) { foreach (var additionalLocation in additionalLocations) { - locationValue = GetLocationValue(additionalLocation); - if (locationValue != null) - { - builder.Add(locationValue); - } + WriteLocation(additionalLocation); } } - return Value.Create(builder.ToImmutableAndFree(), this); + _writer.WriteArrayEnd(); } - private Value GetLocationValue(Location location) + private void WriteLocation(Location location) { if (location.SourceTree == null) { - return null; + return; } - var builder = ArrayBuilder>.GetInstance(); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.LocationSyntaxTreeUri, GetUri(location.SourceTree))); + _writer.WriteObjectStart(); // location + + _writer.WriteArrayStart("analysisTarget"); + _writer.WriteObjectStart(); // physical location component - var spanInfoValue = GetSpanInfoValue(location.GetLineSpan()); - builder.Add(KeyValuePair.Create(WellKnownStrings.LocationSpanInfo, spanInfoValue)); + _writer.Write("uri", GetUri(location.SourceTree)); - var coreLocationValue = Value.Create(builder.ToImmutableAndFree(), this); + // Note that SARIF lines and columns are 1-based, but FileLinePositionSpan is 0-based + FileLinePositionSpan span = location.GetLineSpan(); + _writer.WriteKey("region"); + _writer.WriteObjectStart(); + _writer.Write("startLine", span.StartLinePosition.Line + 1); + _writer.Write("startColumn", span.StartLinePosition.Character + 1); + _writer.Write("endLine", span.EndLinePosition.Line + 1); + _writer.Write("endColumn", span.EndLinePosition.Character + 1); + _writer.WriteObjectEnd(); // region - // Our log format requires this to be wrapped. - var wrapperList = Value.Create(ImmutableArray.Create(coreLocationValue), this); - var wrapperKvp = KeyValuePair.Create(WellKnownStrings.Location, wrapperList); - return Value.Create(ImmutableArray.Create(wrapperKvp), this); + _writer.WriteObjectEnd(); // physical location component + _writer.WriteArrayEnd(); // analysisTarget + _writer.WriteObjectEnd(); // location } private static string GetUri(SyntaxTree syntaxTree) @@ -186,189 +148,87 @@ private static string GetUri(SyntaxTree syntaxTree) return uri.ToString(); } - private Value GetSpanInfoValue(FileLinePositionSpan lineSpan) + private void WriteTags(Diagnostic diagnostic) { - // Note that SARIF region lines and columns are specified to be 1-based, but FileLinePositionSpan.Line and Character are 0-based. - var builder = ArrayBuilder>.GetInstance(); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.LocationSpanStartLine, lineSpan.StartLinePosition.Line + 1)); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.LocationSpanStartColumn, lineSpan.StartLinePosition.Character + 1)); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.LocationSpanEndLine, lineSpan.EndLinePosition.Line + 1)); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.LocationSpanEndColumn, lineSpan.EndLinePosition.Character + 1)); - return Value.Create(builder.ToImmutableAndFree(), this); - } - - private Value GetPropertiesValue(Issue issue) - { - var builder = ArrayBuilder>.GetInstance(); - - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.Severity, issue.Severity.ToString())); - if (issue.Severity == DiagnosticSeverity.Warning) + if (diagnostic.CustomTags.Count > 0) { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.WarningLevel, issue.WarningLevel.ToString())); - } + _writer.WriteArrayStart("tags"); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.DefaultSeverity, issue.DefaultSeverity.ToString())); + foreach (string tag in diagnostic.CustomTags) + { + _writer.Write(tag); + } - if (!string.IsNullOrEmpty(issue.Title)) - { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.Title, issue.Title)); + _writer.WriteArrayEnd(); } + } + + private void WriteProperties(Diagnostic diagnostic, CultureInfo culture) + { + _writer.WriteObjectStart("properties"); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.Category, issue.Category)); + _writer.Write("severity", diagnostic.Severity.ToString()); - if (!string.IsNullOrEmpty(issue.HelpLink)) + if (diagnostic.Severity == DiagnosticSeverity.Warning) { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.HelpLink, issue.HelpLink)); + _writer.Write("warningLevel", diagnostic.WarningLevel.ToString()); } - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.IsEnabledByDefault, issue.IsEnabledByDefault.ToString())); - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.IsSuppressedInSource, issue.IsSuppressedInSource.ToString())); + _writer.Write("defaultSeverity", diagnostic.DefaultSeverity.ToString()); - if (issue.CustomTags.Count > 0) + string title = diagnostic.Descriptor.Title.ToString(culture); + if (!string.IsNullOrEmpty(title)) { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.CustomTags, issue.CustomTags.WhereNotNull().Join(";"))); + _writer.Write("title", title); } - foreach (var kvp in issue.CustomProperties) + _writer.Write("category", diagnostic.Category); + + string helpLink = diagnostic.Descriptor.HelpLinkUri; + if (!string.IsNullOrEmpty(helpLink)) { - builder.Add(CreateSimpleKeyValuePair(WellKnownStrings.CustomProperties + "." + kvp.Key, kvp.Value)); + _writer.Write("helpLink", helpLink); } - return Value.Create(builder.ToImmutableAndFree(), this); - } - - #region Helper methods for core logging - - private void WriteKeyValuePair(KeyValuePair kvp, bool isFirst) - { - WriteKeyValuePair(kvp.Key, kvp.Value, isFirst); - } - - private void WriteKeyValuePair(string key, Value value, bool isFirst) - { - WriteKey(key, isFirst); - WriteValue(value); - } - - private void WriteSimpleKeyValuePair(string key, string value, bool isFirst) - { - WriteKey(key, isFirst); - WriteValue(value); - } + _writer.Write("isEnabledByDefault", diagnostic.IsEnabledByDefault.ToString()); - private void WriteKey(string key, bool isFirst) - { - StartNewEntry(isFirst); - _writer.Write($"\"{key}\": "); - } - - private void WriteValue(Value value, bool isFirst = true, bool valueInList = false) - { - if (!isFirst || valueInList) + foreach (var pair in diagnostic.Properties.OrderBy(x => x.Key, StringComparer.Ordinal)) { - StartNewEntry(isFirst); + _writer.Write("customProperties." + pair.Key, pair.Value); } - value.Write(); - } - - private void WriteValue(string value) - { - _writer.Flush(); - _jsonStringSerializer.WriteObject(_writer.BaseStream, value); - } - - private void WriteValue(int value) - { - _writer.Write(value); + _writer.WriteObjectEnd(); // properties } - private void StartNewEntry(bool isFirst) + private static string GetKind(DiagnosticSeverity severity) { - if (!isFirst) - { - _writer.WriteLine(','); - } - else + switch (severity) { - _writer.WriteLine(); + case DiagnosticSeverity.Info: + return "note"; + + case DiagnosticSeverity.Error: + return "error"; + + case DiagnosticSeverity.Warning: + case DiagnosticSeverity.Hidden: + default: + // note that in the hidden or default cases, we still write out the actual severity as a + // property so no information is lost. We have to conform to the SARIF spec for kind, + // which allows only pass, warning, error, or notApplicable. + return "warning"; } - - _writer.Write(_currentIndent); - } - - private void StartGroup() - { - StartGroupOrListCommon(groupStartChar); - } - - private void EndGroup() - { - EndGroupOrListCommon(groupEndChar); - } - - private void StartList() - { - StartGroupOrListCommon(listStartChar); - } - - private void EndList() - { - EndGroupOrListCommon(listEndChar); } - private void StartGroupOrListCommon(char startChar) - { - _writer.Write(startChar); - IncreaseIndentation(); - } - - private void EndGroupOrListCommon(char endChar) - { - _writer.WriteLine(); - DecreaseIndentation(); - _writer.Write(_currentIndent + endChar); - } - - private void IncreaseIndentation() - { - _currentIndent += indentDelta; - } - - private void DecreaseIndentation() - { - _currentIndent = _currentIndent.Substring(indentDelta.Length); - } - - private KeyValuePair CreateSimpleKeyValuePair(string key, string value) - { - var stringValue = Value.Create(value, this); - return KeyValuePair.Create(key, stringValue); - } - - private KeyValuePair CreateSimpleKeyValuePair(string key, int value) - { - var intValue = Value.Create(value, this); - return KeyValuePair.Create(key, intValue); - } - - #endregion - public void Dispose() { - // End issues list. - EndList(); - - // End runLog entry. - EndGroup(); - - // End runLogs list. - EndList(); - - // End dictionary for log file key-value pairs. - EndGroup(); - + _writer.WriteArrayEnd(); // results + _writer.WriteObjectEnd(); // runLog + _writer.WriteArrayEnd(); // runLogs + _writer.WriteObjectEnd(); // root _writer.Dispose(); } } } + +#pragma warning restore RS0013 \ No newline at end of file diff --git a/src/Compilers/Core/Portable/InternalUtilities/JsonWriter.cs b/src/Compilers/Core/Portable/InternalUtilities/JsonWriter.cs new file mode 100644 index 0000000000000000000000000000000000000000..1a5f0faad02577c743f3333bf2ecfbf57eb167f8 --- /dev/null +++ b/src/Compilers/Core/Portable/InternalUtilities/JsonWriter.cs @@ -0,0 +1,158 @@ +// 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.IO; +using System.Runtime.Serialization.Json; + +namespace Roslyn.Utilities +{ + /// + /// A simple, forward-only JSON writer to avoid adding dependencies to the compiler. + /// Used to generate /errorlogger output. + /// + /// Does not guarantee well-formed JSON if misused. It is the caller's reponsibility + /// to balance array/object start/end, to only write key-value pairs to objects and + /// elements to arrays, etc. + /// + /// Takes ownership of the given StreamWriter at construction and handles its disposal. + /// + internal sealed class JsonWriter : IDisposable + { + private readonly StreamWriter _outptut; + private readonly DataContractJsonSerializer _stringWriter; + private int _indent; + private string _pending; + + private static readonly string s_newLine = Environment.NewLine; + private static readonly string s_commaNewLine = "," + Environment.NewLine; + + private const string Indentation = " "; + + public JsonWriter(StreamWriter output) + { + _outptut = output; + _stringWriter = new DataContractJsonSerializer(typeof(string)); + _pending = ""; + } + + public void WriteObjectStart() + { + WriteStart('{'); + } + + public void WriteObjectStart(string key) + { + WriteKey(key); + WriteObjectStart(); + } + + public void WriteObjectEnd() + { + WriteEnd('}'); + } + + public void WriteArrayStart() + { + WriteStart('['); + } + + public void WriteArrayStart(string key) + { + WriteKey(key); + WriteArrayStart(); + } + + public void WriteArrayEnd() + { + WriteEnd(']'); + } + + public void WriteKey(string key) + { + Write(key); + _outptut.Write(": "); + _pending = ""; + } + + public void Write(string key, string value) + { + WriteKey(key); + Write(value); + } + + public void Write(string key, int value) + { + WriteKey(key); + Write(value); + } + + public void Write(string key, bool value) + { + WriteKey(key); + Write(value); + } + + public void Write(string value) + { + // Consider switching to custom escaping logic here. Flushing all the time (in + // order to borrow DataContractJsonSerializer escaping) is expensive. + // + // Also, it would be nicer not to escape the forward slashes in URIs (which is + // optional in JSON.) + + WritePending(); + _outptut.Flush(); + _stringWriter.WriteObject(_outptut.BaseStream, value); + _pending = s_commaNewLine; + } + + public void Write(int value) + { + WritePending(); + _outptut.Write(value); + _pending = s_commaNewLine; + } + + public void Write(bool value) + { + WritePending(); + _outptut.Write(value ? "true" : "false"); + _pending = s_commaNewLine; + } + + private void WritePending() + { + if (_pending.Length > 0) + { + _outptut.Write(_pending); + + for (int i = 0; i < _indent; i++) + { + _outptut.Write(Indentation); + } + } + } + + private void WriteStart(char c) + { + WritePending(); + _outptut.Write(c); + _pending = s_newLine; + _indent++; + } + + private void WriteEnd(char c) + { + _pending = s_newLine; + _indent--; + WritePending(); + _outptut.Write(c); + _pending = s_commaNewLine; + } + + public void Dispose() + { + _outptut.Dispose(); + } + } +} diff --git a/src/Compilers/VisualBasic/Test/CommandLine/ErrorLoggerTests.vb b/src/Compilers/VisualBasic/Test/CommandLine/ErrorLoggerTests.vb index 39be5d80273b8311813619cabdbc48fa2dbd64f2..ce71c683d7a04fd1f2f8970ceeb595f4976f5902 100644 --- a/src/Compilers/VisualBasic/Test/CommandLine/ErrorLoggerTests.vb +++ b/src/Compilers/VisualBasic/Test/CommandLine/ErrorLoggerTests.vb @@ -46,7 +46,7 @@ End Class Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedIssues = " - ""issues"": [ + ""results"": [ ] } ] @@ -91,9 +91,10 @@ End Class Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedIssues = String.Format(" - ""issues"": [ + ""results"": [ {{ ""ruleId"": ""BC42024"", + ""kind"": ""warning"", ""locations"": [ {{ ""analysisTarget"": [ @@ -110,29 +111,37 @@ End Class }} ], ""fullMessage"": ""Unused local variable: 'x'."", + ""isSuppressedInSource"": false, + ""tags"": [ + ""Compiler"", + ""Telemetry"" + ], ""properties"": {{ ""severity"": ""Warning"", ""warningLevel"": ""1"", ""defaultSeverity"": ""Warning"", ""title"": ""Unused local variable"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""False"", - ""customTags"": ""Compiler;Telemetry"" + ""isEnabledByDefault"": ""True"" }} }}, {{ ""ruleId"": ""BC30420"", + ""kind"": ""error"", ""locations"": [ ], ""fullMessage"": ""'Sub Main' was not found in '{1}'."", + ""isSuppressedInSource"": false, + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ], ""properties"": {{ ""severity"": ""Error"", ""defaultSeverity"": ""Error"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""False"", - ""customTags"": ""Compiler;Telemetry;NotConfigurable"" + ""isEnabledByDefault"": ""True"" }} }} ] @@ -182,9 +191,10 @@ End Class Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedIssues = String.Format(" - ""issues"": [ + ""results"": [ {{ ""ruleId"": ""BC42024"", + ""kind"": ""warning"", ""locations"": [ {{ ""analysisTarget"": [ @@ -201,29 +211,37 @@ End Class }} ], ""fullMessage"": ""Unused local variable: 'x'."", + ""isSuppressedInSource"": true, + ""tags"": [ + ""Compiler"", + ""Telemetry"" + ], ""properties"": {{ ""severity"": ""Warning"", ""warningLevel"": ""1"", ""defaultSeverity"": ""Warning"", ""title"": ""Unused local variable"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""True"", - ""customTags"": ""Compiler;Telemetry"" + ""isEnabledByDefault"": ""True"" }} }}, {{ ""ruleId"": ""BC30420"", + ""kind"": ""error"", ""locations"": [ ], ""fullMessage"": ""'Sub Main' was not found in '{1}'."", + ""isSuppressedInSource"": false, + ""tags"": [ + ""Compiler"", + ""Telemetry"", + ""NotConfigurable"" + ], ""properties"": {{ ""severity"": ""Error"", ""defaultSeverity"": ""Error"", ""category"": ""Compiler"", - ""isEnabledByDefault"": ""True"", - ""isSuppressedInSource"": ""False"", - ""customTags"": ""Compiler;Telemetry;NotConfigurable"" + ""isEnabledByDefault"": ""True"" }} }} ] @@ -271,7 +289,7 @@ End Class Dim actualOutput = File.ReadAllText(errorLogFile).Trim() Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) - Dim expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogIssuesText(cmd.Compilation) + Dim expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogResultsText(cmd.Compilation) Dim expectedText = expectedHeader + expectedIssues Assert.Equal(expectedText, actualOutput) diff --git a/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs b/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs index 0b13373e9d2b4bb7264989c74701031ce79211e8..3566aa5f4306f48789866177fe982b2875a884aa 100644 --- a/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs +++ b/src/Test/Utilities/Desktop/CommonDiagnosticAnalyzers.cs @@ -77,7 +77,7 @@ private static string GetExpectedPropertiesMapText() return expectedText; } - public static string GetExpectedErrorLogIssuesText(Compilation compilation) + public static string GetExpectedErrorLogResultsText(Compilation compilation) { var tree = compilation.SyntaxTrees.First(); var root = tree.GetRoot(); @@ -85,9 +85,10 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation) var filePath = GetEscapedUriForPath(tree.FilePath); return @" - ""issues"": [ + ""results"": [ { ""ruleId"": """ + Descriptor1.Id + @""", + ""kind"": """ + (Descriptor1.DefaultSeverity == DiagnosticSeverity.Error ? "error" : "warning") + @""", ""locations"": [ { ""analysisTarget"": [ @@ -105,6 +106,10 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation) ], ""shortMessage"": """ + Descriptor1.MessageFormat + @""", ""fullMessage"": """ + Descriptor1.Description + @""", + ""isSuppressedInSource"": false, + ""tags"": [ + " + String.Join("," + Environment.NewLine + " ", Descriptor1.CustomTags.Select(s => $"\"{s}\"")) + @" + ], ""properties"": { ""severity"": """ + Descriptor1.DefaultSeverity + @""", ""warningLevel"": ""1"", @@ -112,27 +117,28 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation) ""title"": """ + Descriptor1.Title + @""", ""category"": """ + Descriptor1.Category + @""", ""helpLink"": """ + Descriptor1.HelpLinkUri + @""", - ""isEnabledByDefault"": """ + Descriptor1.IsEnabledByDefault + @""", - ""isSuppressedInSource"": ""False"", - ""customTags"": """ + Descriptor1.CustomTags.Join(";") + @"""" + + ""isEnabledByDefault"": """ + Descriptor1.IsEnabledByDefault + @"""" + GetExpectedPropertiesMapText() + @" } }, { ""ruleId"": """ + Descriptor2.Id + @""", + ""kind"": """ + (Descriptor2.DefaultSeverity == DiagnosticSeverity.Error ? "error" : "warning") + @""", ""locations"": [ ], ""shortMessage"": """ + Descriptor2.MessageFormat + @""", ""fullMessage"": """ + Descriptor2.Description + @""", + ""isSuppressedInSource"": false, + ""tags"": [ + " + String.Join("," + Environment.NewLine + " ", Descriptor2.CustomTags.Select(s => $"\"{s}\"")) + @" + ], ""properties"": { ""severity"": """ + Descriptor2.DefaultSeverity + @""", ""defaultSeverity"": """ + Descriptor2.DefaultSeverity + @""", ""title"": """ + Descriptor2.Title + @""", ""category"": """ + Descriptor2.Category + @""", ""helpLink"": """ + Descriptor2.HelpLinkUri + @""", - ""isEnabledByDefault"": """ + Descriptor2.IsEnabledByDefault + @""", - ""isSuppressedInSource"": ""False"", - ""customTags"": """ + Descriptor2.CustomTags.Join(";") + @"""" + + ""isEnabledByDefault"": """ + Descriptor2.IsEnabledByDefault + @"""" + GetExpectedPropertiesMapText() + @" } }