提交 cd21f84c 编写于 作者: N Nick Guerrera

Merge pull request #11390 from nguerrera/duplicate-rule-metadata

/errorlog improvements, fixes, and marking as v1.0.0
Introduction
============
The C# and Visual Basic compilers support a /errorlog:<file> 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
// 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<Location> additionalLocations = new[] { additionalLocation };
IEnumerable<Location> 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
......
......@@ -84,6 +84,11 @@ internal Version GetAssemblyVersion()
return typeof(CommonCompiler).GetTypeInfo().Assembly.GetName().Version;
}
internal string GetCultureName()
{
return Culture.Name;
}
internal virtual Func<string, MetadataReferenceProperties, PortableExecutableReference> GetMetadataProvider()
{
return (path, properties) => MetadataReference.CreateFromFile(path, properties);
......
......@@ -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<string, int> _counters = new Dictionary<string, int>();
// DiagnosticDescriptor -> unique key
private Dictionary<DiagnosticDescriptor, string> _keys = new Dictionary<DiagnosticDescriptor, string>();
private Dictionary<DiagnosticDescriptor, string> _keys = new Dictionary<DiagnosticDescriptor, string>(new Comparer());
/// <summary>
/// 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;
}
/// <summary>
/// Compares descriptors by the values that we write to the log and nothing else.
///
/// We cannot just use <see cref="DiagnosticDescriptor"/>'s built-in implementation
/// of <see cref="IEquatable{DiagnosticDescriptor}"/> for two reasons:
///
/// 1. <see cref="DiagnosticDescriptor.MessageFormat"/> 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. <see cref="DiagnosticDescriptor.CustomTags"/> 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.
/// </summary>
private sealed class Comparer : IEqualityComparer<DiagnosticDescriptor>
{
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))))))));
}
}
}
}
}
......
......@@ -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)
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册