diff --git a/docs/compilers/Error Log Format.md b/docs/compilers/Error Log Format.md index ecbace7530ffc3120971157d00ad6dd79932d0a1..f8ee6d0309bc75f850fbc0d91ddeb88eb422f0b1 100644 --- a/docs/compilers/Error Log Format.md +++ b/docs/compilers/Error Log Format.md @@ -1,31 +1,6 @@ -Introduction -============ The C# and Visual Basic compilers support a /errorlog: switch on the command line to log all diagnostics in a structured, JSON format. -The log format is SARIF (Static Analysis Results Interchange Format) -and is defined by https://github.com/sarif-standard/sarif-spec - -Note that the format has not been finalized and the specification is -still a draft. It will remain subject to breaking changes until the -`version` property is emitted with a value of "1.0" or greater. - -This document does not repeat the details of the SARIF format, but -rather adds information that is specific to the implementation provided -by the C# and Visual Basic Compilers. - - -Result Properties -================ -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 -the corresponding `Microsoft.CodeAnalysis.Diagnostic` as follows: - -Key | Value ------------------------- | ------------ -"warningLevel" | `Diagnostic.WarningLevel` -"category" | `Diagnostic.Category` -"isEnabledByDefault" | `Diagnostic.IsEnabledByDefault -"customProperties" | `Diagnostic.Properties` +The log format is SARIF (Static Analysis Results Interchange Format): +See https://sarifweb.azurewebsites.net/ for the format specification, +JSON schema, and other related resources. \ No newline at end of file diff --git a/src/Compilers/Core/CodeAnalysisTest/Diagnostics/ErrorLoggerTests.cs b/src/Compilers/Core/CodeAnalysisTest/Diagnostics/ErrorLoggerTests.cs index 88d89fdeba71ae0f10f74d4b98c8fa76a5329263..2bdaebd0ab88707467a462f47500c69a12bcd13b 100644 --- a/src/Compilers/Core/CodeAnalysisTest/Diagnostics/ErrorLoggerTests.cs +++ b/src/Compilers/Core/CodeAnalysisTest/Diagnostics/ErrorLoggerTests.cs @@ -1,13 +1,14 @@ // 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 Xunit; -using System.IO; using System; +using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Text; -using Microsoft.CodeAnalysis.CSharp; + using Microsoft.CodeAnalysis.Text; -using System.Collections.Generic; + +using Xunit; namespace Microsoft.CodeAnalysis.UnitTests.Diagnostics { @@ -18,27 +19,33 @@ public class ErrorLoggerTests public void AdditionalLocationsAsRelatedLocations() { var stream = new MemoryStream(); - using (var logger = new ErrorLogger(stream, "toolName", "1.2.3.4", new Version(1, 2, 3, 4), CultureInfo.InvariantCulture)) + using (var logger = new ErrorLogger(stream, "toolName", "1.2.3.4", new Version(1, 2, 3, 4), new CultureInfo("fr-CA"))) { - var mainLocation = Location.Create(@"Z:\MainLocation.cs", new TextSpan(0, 0), new LinePositionSpan(LinePosition.Zero, LinePosition.Zero)); - var additionalLocation = Location.Create(@"Z:\AdditionalLocation.cs", new TextSpan(0, 0), new LinePositionSpan(LinePosition.Zero, LinePosition.Zero)); + var span = new TextSpan(0, 0); + var position = new LinePositionSpan(LinePosition.Zero, LinePosition.Zero); + var mainLocation = Location.Create(@"Z:\Main Location.cs", span, position); var descriptor = new DiagnosticDescriptor("TST", "_TST_", "", "", DiagnosticSeverity.Error, false); - IEnumerable additionalLocations = new[] { additionalLocation }; + IEnumerable additionalLocations = new[] { + Location.Create(@"Relative Additional\Location.cs", span, position), + Location.Create(@"a:cannot/interpret/as\uri", span, position), + }; + logger.LogDiagnostic(Diagnostic.Create(descriptor, mainLocation, additionalLocations)); } - string expected = + string expected = @"{ - ""$schema"": ""http://json.schemastore.org/sarif-1.0.0-beta.5"", - ""version"": ""1.0.0-beta.5"", + ""$schema"": ""http://json.schemastore.org/sarif-1.0.0"", + ""version"": ""1.0.0"", ""runs"": [ { ""tool"": { ""name"": ""toolName"", ""version"": ""1.2.3.4"", ""fileVersion"": ""1.2.3.4"", - ""semanticVersion"": ""1.2.3"" + ""semanticVersion"": ""1.2.3"", + ""language"": ""fr-CA"" }, ""results"": [ { @@ -47,7 +54,7 @@ public void AdditionalLocationsAsRelatedLocations() ""locations"": [ { ""resultFile"": { - ""uri"": ""file:///Z:/MainLocation.cs"", + ""uri"": ""file:///Z:/Main%20Location.cs"", ""region"": { ""startLine"": 1, ""startColumn"": 1, @@ -60,7 +67,18 @@ public void AdditionalLocationsAsRelatedLocations() ""relatedLocations"": [ { ""physicalLocation"": { - ""uri"": ""file:///Z:/AdditionalLocation.cs"", + ""uri"": ""Relative%20Additional/Location.cs"", + ""region"": { + ""startLine"": 1, + ""startColumn"": 1, + ""endLine"": 1, + ""endColumn"": 1 + } + } + }, + { + ""physicalLocation"": { + ""uri"": ""a%3Acannot%2Finterpret%2Fas%5Curi"", ""region"": { ""startLine"": 1, ""startColumn"": 1, @@ -93,14 +111,29 @@ public void AdditionalLocationsAsRelatedLocations() public void DescriptorIdCollision() { var descriptors = new[] { - new DiagnosticDescriptor("TST001.001", "_TST001.001_", "", "", DiagnosticSeverity.Warning, true), + // Toughest case: generation of TST001-001 collides with with actual TST001-001 and must be bumped to TST001-002 + new DiagnosticDescriptor("TST001-001", "_TST001-001_", "", "", DiagnosticSeverity.Warning, true), new DiagnosticDescriptor("TST001", "_TST001_", "", "", DiagnosticSeverity.Warning, true), - new DiagnosticDescriptor("TST001", "_TST001.002_", "", "", DiagnosticSeverity.Warning, true), - new DiagnosticDescriptor("TST001", "_TST001.003_", "", "", DiagnosticSeverity.Warning, true), + new DiagnosticDescriptor("TST001", "_TST001-002_", "", "", DiagnosticSeverity.Warning, true), + new DiagnosticDescriptor("TST001", "_TST001-003_", "", "", DiagnosticSeverity.Warning, true), + + // Descriptors with same values should not get distinct entries in log + new DiagnosticDescriptor("TST002", "", "", "", DiagnosticSeverity.Warning, true), + new DiagnosticDescriptor("TST002", "", "", "", DiagnosticSeverity.Warning, true), + + // Changing only the message format (which we do not write out) should not produce a distinct entry in log. + new DiagnosticDescriptor("TST002", "", "messageFormat", "", DiagnosticSeverity.Warning, true), + + // Changing any property that we do write out should create a distinct entry + new DiagnosticDescriptor("TST002", "title_001", "", "", DiagnosticSeverity.Warning, true), + new DiagnosticDescriptor("TST002", "", "", "category_002", DiagnosticSeverity.Warning, true), + new DiagnosticDescriptor("TST002", "", "", "", DiagnosticSeverity.Error /*003*/, true), + new DiagnosticDescriptor("TST002", "", "", "", DiagnosticSeverity.Warning, isEnabledByDefault: false /*004*/), + new DiagnosticDescriptor("TST002", "", "", "", DiagnosticSeverity.Warning, true, "description_005"), }; var stream = new MemoryStream(); - using (var logger = new ErrorLogger(stream, "toolName", "1.2.3.4", new Version(1, 2, 3, 4), CultureInfo.InvariantCulture)) + using (var logger = new ErrorLogger(stream, "toolName", "1.2.3.4", new Version(1, 2, 3, 4), new CultureInfo("en-US"))) { for (int i = 0; i < 2; i++) { @@ -113,19 +146,20 @@ public void DescriptorIdCollision() string expected = @"{ - ""$schema"": ""http://json.schemastore.org/sarif-1.0.0-beta.5"", - ""version"": ""1.0.0-beta.5"", + ""$schema"": ""http://json.schemastore.org/sarif-1.0.0"", + ""version"": ""1.0.0"", ""runs"": [ { ""tool"": { ""name"": ""toolName"", ""version"": ""1.2.3.4"", ""fileVersion"": ""1.2.3.4"", - ""semanticVersion"": ""1.2.3"" + ""semanticVersion"": ""1.2.3"", + ""language"": ""en-US"" }, ""results"": [ { - ""ruleId"": ""TST001.001"", + ""ruleId"": ""TST001-001"", ""level"": ""warning"", ""properties"": { ""warningLevel"": 1 @@ -140,7 +174,7 @@ public void DescriptorIdCollision() }, { ""ruleId"": ""TST001"", - ""ruleKey"": ""TST001.002"", + ""ruleKey"": ""TST001-002"", ""level"": ""warning"", ""properties"": { ""warningLevel"": 1 @@ -148,14 +182,73 @@ public void DescriptorIdCollision() }, { ""ruleId"": ""TST001"", - ""ruleKey"": ""TST001.003"", + ""ruleKey"": ""TST001-003"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""level"": ""warning"", + ""message"": ""messageFormat"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-001"", ""level"": ""warning"", ""properties"": { ""warningLevel"": 1 } }, { - ""ruleId"": ""TST001.001"", + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-002"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-003"", + ""level"": ""error"" + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-004"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-005"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST001-001"", ""level"": ""warning"", ""properties"": { ""warningLevel"": 1 @@ -170,7 +263,7 @@ public void DescriptorIdCollision() }, { ""ruleId"": ""TST001"", - ""ruleKey"": ""TST001.002"", + ""ruleKey"": ""TST001-002"", ""level"": ""warning"", ""properties"": { ""warningLevel"": 1 @@ -178,7 +271,66 @@ public void DescriptorIdCollision() }, { ""ruleId"": ""TST001"", - ""ruleKey"": ""TST001.003"", + ""ruleKey"": ""TST001-003"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""level"": ""warning"", + ""message"": ""messageFormat"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-001"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-002"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-003"", + ""level"": ""error"" + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-004"", + ""level"": ""warning"", + ""properties"": { + ""warningLevel"": 1 + } + }, + { + ""ruleId"": ""TST002"", + ""ruleKey"": ""TST002-005"", ""level"": ""warning"", ""properties"": { ""warningLevel"": 1 @@ -194,25 +346,70 @@ public void DescriptorIdCollision() ""isEnabledByDefault"": true } }, - ""TST001.001"": { - ""id"": ""TST001.001"", - ""shortDescription"": ""_TST001.001_"", + ""TST001-001"": { + ""id"": ""TST001-001"", + ""shortDescription"": ""_TST001-001_"", ""defaultLevel"": ""warning"", ""properties"": { ""isEnabledByDefault"": true } }, - ""TST001.002"": { + ""TST001-002"": { ""id"": ""TST001"", - ""shortDescription"": ""_TST001.002_"", + ""shortDescription"": ""_TST001-002_"", ""defaultLevel"": ""warning"", ""properties"": { ""isEnabledByDefault"": true } }, - ""TST001.003"": { + ""TST001-003"": { ""id"": ""TST001"", - ""shortDescription"": ""_TST001.003_"", + ""shortDescription"": ""_TST001-003_"", + ""defaultLevel"": ""warning"", + ""properties"": { + ""isEnabledByDefault"": true + } + }, + ""TST002"": { + ""id"": ""TST002"", + ""defaultLevel"": ""warning"", + ""properties"": { + ""isEnabledByDefault"": true + } + }, + ""TST002-001"": { + ""id"": ""TST002"", + ""shortDescription"": ""title_001"", + ""defaultLevel"": ""warning"", + ""properties"": { + ""isEnabledByDefault"": true + } + }, + ""TST002-002"": { + ""id"": ""TST002"", + ""defaultLevel"": ""warning"", + ""properties"": { + ""category"": ""category_002"", + ""isEnabledByDefault"": true + } + }, + ""TST002-003"": { + ""id"": ""TST002"", + ""defaultLevel"": ""error"", + ""properties"": { + ""isEnabledByDefault"": true + } + }, + ""TST002-004"": { + ""id"": ""TST002"", + ""defaultLevel"": ""warning"", + ""properties"": { + ""isEnabledByDefault"": false + } + }, + ""TST002-005"": { + ""id"": ""TST002"", + ""fullDescription"": ""description_005"", ""defaultLevel"": ""warning"", ""properties"": { ""isEnabledByDefault"": true diff --git a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs index eb5b292ebeeb0adf7a3e848c0f62e49c9c805f42..aac472bb3fdde0484220f4b2b014e8c05efd8c8a 100644 --- a/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs +++ b/src/Compilers/Core/Portable/CommandLine/CommonCompiler.cs @@ -84,6 +84,11 @@ internal Version GetAssemblyVersion() return typeof(CommonCompiler).GetTypeInfo().Assembly.GetName().Version; } + internal string GetCultureName() + { + return Culture.Name; + } + internal virtual Func GetMetadataProvider() { return (path, properties) => MetadataReference.CreateFromFile(path, properties); diff --git a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs index f13244b233105b1ecf30f1358e1f377320708806..8c7b9917d784ac7918f610e41b4231d5089c51c6 100644 --- a/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs +++ b/src/Compilers/Core/Portable/CommandLine/ErrorLogger.cs @@ -37,26 +37,22 @@ public ErrorLogger(Stream stream, string toolName, string toolFileVersion, Versi _culture = culture; _writer.WriteObjectStart(); // root - _writer.Write("$schema", "http://json.schemastore.org/sarif-1.0.0-beta.5"); - _writer.Write("version", "1.0.0-beta.5"); + _writer.Write("$schema", "http://json.schemastore.org/sarif-1.0.0"); + _writer.Write("version", "1.0.0"); _writer.WriteArrayStart("runs"); _writer.WriteObjectStart(); // run - WriteToolInfo(toolName, toolFileVersion, toolAssemblyVersion); + _writer.WriteObjectStart("tool"); + _writer.Write("name", toolName); + _writer.Write("version", toolAssemblyVersion.ToString()); + _writer.Write("fileVersion", toolFileVersion); + _writer.Write("semanticVersion", toolAssemblyVersion.ToString(fieldCount: 3)); + _writer.Write("language", culture.Name); + _writer.WriteObjectEnd(); // tool _writer.WriteArrayStart("results"); } - private void WriteToolInfo(string name, string fileVersion, Version assemblyVersion) - { - _writer.WriteObjectStart("tool"); - _writer.Write("name", name); - _writer.Write("version", assemblyVersion.ToString()); - _writer.Write("fileVersion", fileVersion); - _writer.Write("semanticVersion", assemblyVersion.ToString(fieldCount: 3)); - _writer.WriteObjectEnd(); - } - public void LogDiagnostic(Diagnostic diagnostic) { _writer.WriteObjectStart(); // result @@ -155,22 +151,36 @@ private static bool HasPath(Location location) return !string.IsNullOrEmpty(location.GetLineSpan().Path); } + private static readonly Uri _fileRoot = new Uri("file:///"); + private static string GetUri(string path) { Debug.Assert(!string.IsNullOrEmpty(path)); + // Note that in general, these "paths" are opaque strings to be + // interpreted by resolvers (see SyntaxTree.FilePath documentation). + + // Common case: absolute path -> absolute URI Uri uri; + if (Uri.TryCreate(path, UriKind.Absolute, out uri)) + { + // We use Uri.AbsoluteUri and not Uri.ToString() because Uri.ToString() + // is unescaped (e.g. spaces remain unreplaced by %20) and therefore + // not well-formed. + return uri.AbsoluteUri; + } - if (!Uri.TryCreate(path, UriKind.RelativeOrAbsolute, out uri)) + // First fallback attempt: attempt to interpret as relative path/URI. + // (Perhaps the resolver works that way.) + if (Uri.TryCreate(path, UriKind.Relative, out uri)) { - // The only constraint on paths are that they can be interpreted by - // various resolvers so there is no guarantee we can turn the arbitrary string - // in to a URI. If our attempt to do so fails, use the original string as the - // "URI". - return path; + // There is no AbsoluteUri equivalent for relative URI references and ToString() + // won't escape without this relative -> absolute -> relative trick. + return _fileRoot.MakeRelativeUri(new Uri(_fileRoot, uri)).ToString(); } - return uri.ToString(); + // Last resort: UrlEncode the whole opaque string. + return System.Net.WebUtility.UrlEncode(path); } private void WriteProperties(Diagnostic diagnostic) @@ -317,7 +327,7 @@ private sealed class DiagnosticDescriptorSet private Dictionary _counters = new Dictionary(); // DiagnosticDescriptor -> unique key - private Dictionary _keys = new Dictionary(); + private Dictionary _keys = new Dictionary(new Comparer()); /// /// The total number of descriptors in the set. @@ -355,7 +365,7 @@ public string Add(DiagnosticDescriptor descriptor) do { _counters[descriptor.Id] = ++counter; - key = descriptor.Id + "." + counter.ToString("000", CultureInfo.InvariantCulture); + key = descriptor.Id + "-" + counter.ToString("000", CultureInfo.InvariantCulture); } while (_counters.ContainsKey(key)); _keys.Add(descriptor, key); @@ -381,6 +391,72 @@ public string Add(DiagnosticDescriptor descriptor) list.Sort((x, y) => string.CompareOrdinal(x.Key, y.Key)); return list; } + + /// + /// Compares descriptors by the values that we write to the log and nothing else. + /// + /// We cannot just use 's built-in implementation + /// of for two reasons: + /// + /// 1. is part of that built-in + /// equatability, but we do not write it out, and so descriptors differing only + /// by MessageFormat (common) would lead to duplicate rule metadata entries in + /// the log. + /// + /// 2. is *not* part of that built-in + /// equatability, but we do write them out, and so descriptors differening only + /// by CustomTags (rare) would cause only one set of tags to be reported in the + /// log. + /// + private sealed class Comparer : IEqualityComparer + { + public bool Equals(DiagnosticDescriptor x, DiagnosticDescriptor y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (ReferenceEquals(x, null) || ReferenceEquals(y, null)) + { + return false; + } + + // The properties are guaranteed to be non-null by DiagnosticDescriptor invariants. + Debug.Assert(x.Description != null && x.Title != null && x.CustomTags != null); + Debug.Assert(y.Description != null && y.Title != null && y.CustomTags != null); + + return (x.Category == y.Category + && x.DefaultSeverity == y.DefaultSeverity + && x.Description.Equals(y.Description) + && x.HelpLinkUri == y.HelpLinkUri + && x.Id == y.Id + && x.IsEnabledByDefault == y.IsEnabledByDefault + && x.Title.Equals(y.Title) + && x.CustomTags.SequenceEqual(y.CustomTags)); + } + + public int GetHashCode(DiagnosticDescriptor obj) + { + if (ReferenceEquals(obj, null)) + { + return 0; + } + + // The properties are guaranteed to be non-null by DiagnosticDescriptor invariants. + Debug.Assert(obj.Category != null && obj.Description != null && obj.HelpLinkUri != null + && obj.Id != null && obj.Title != null && obj.CustomTags != null); + + return Hash.Combine(obj.Category.GetHashCode(), + Hash.Combine(obj.DefaultSeverity.GetHashCode(), + Hash.Combine(obj.Description.GetHashCode(), + Hash.Combine(obj.HelpLinkUri.GetHashCode(), + Hash.Combine(obj.Id.GetHashCode(), + Hash.Combine(obj.IsEnabledByDefault.GetHashCode(), + Hash.Combine(obj.Title.GetHashCode(), + Hash.CombineValues(obj.CustomTags)))))))); + } + } } } } diff --git a/src/Test/Utilities/Shared/Diagnostics/DiagnosticExtensions.cs b/src/Test/Utilities/Shared/Diagnostics/DiagnosticExtensions.cs index 0e2bddd20d38ac6ad9fc957b34596fbc6a316849..ff080aff3b6a0dde754376e7cf371fab3e7e58dd 100644 --- a/src/Test/Utilities/Shared/Diagnostics/DiagnosticExtensions.cs +++ b/src/Test/Utilities/Shared/Diagnostics/DiagnosticExtensions.cs @@ -308,18 +308,20 @@ internal static string GetExpectedErrorLogHeader(string actualOutput, CommonComp var expectedVersion = compiler.GetAssemblyVersion(); var expectedSemanticVersion = compiler.GetAssemblyVersion().ToString(fieldCount: 3); var expectedFileVersion = compiler.GetAssemblyFileVersion(); + var expectedLanguage = compiler.GetCultureName(); return string.Format(@"{{ - ""$schema"": ""http://json.schemastore.org/sarif-1.0.0-beta.5"", - ""version"": ""1.0.0-beta.5"", + ""$schema"": ""http://json.schemastore.org/sarif-1.0.0"", + ""version"": ""1.0.0"", ""runs"": [ {{ ""tool"": {{ ""name"": ""{0}"", ""version"": ""{1}"", ""fileVersion"": ""{2}"", - ""semanticVersion"": ""{3}"" - }},", expectedToolName, expectedVersion, expectedFileVersion, expectedSemanticVersion); + ""semanticVersion"": ""{3}"", + ""language"": ""{4}"" + }},", expectedToolName, expectedVersion, expectedFileVersion, expectedSemanticVersion, expectedLanguage); } public static string Stringize(this Diagnostic e)