提交 4a03fc08 编写于 作者: N Nick Guerrera

Bring /errorlog up to SARIF v0.4 draft

* "issues" -> "results"
* "isSuppressedInSource" from custom property to first class
* deduce "kind" from severity
上级 87e2fc92
...@@ -15,9 +15,9 @@ rather adds information that is specific to the implementation provided ...@@ -15,9 +15,9 @@ rather adds information that is specific to the implementation provided
by the C# and Visual Basic Compilers. 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. to contain arbitrary (string, string) key-value pairs.
The keys and values used by the C# and VB compilers are serialized from The keys and values used by the C# and VB compilers are serialized from
...@@ -33,6 +33,4 @@ Key | Value ...@@ -33,6 +33,4 @@ Key | Value
"category" | `Diagnostic.Category` "category" | `Diagnostic.Category`
"helpLink" | `DiagnosticDescriptor.HelpLink` (omitted if null or empty) "helpLink" | `DiagnosticDescriptor.HelpLink` (omitted if null or empty)
"isEnabledByDefault" | `Diagnostic.IsEnabledByDefault` ("True" or "False") "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) "customProperties.[key]" | `Diagnostic.Properties[key]` (for each key in the dictionary)
...@@ -46,7 +46,7 @@ public static void Main(string[] args) ...@@ -46,7 +46,7 @@ public static void Main(string[] args)
var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd);
var expectedIssues = @" var expectedIssues = @"
""issues"": [ ""results"": [
] ]
} }
] ]
...@@ -85,9 +85,10 @@ public class C ...@@ -85,9 +85,10 @@ public class C
var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd);
var expectedIssues = string.Format(@" var expectedIssues = string.Format(@"
""issues"": [ ""results"": [
{{ {{
""ruleId"": ""CS0169"", ""ruleId"": ""CS0169"",
""kind"": ""warning"",
""locations"": [ ""locations"": [
{{ {{
""analysisTarget"": [ ""analysisTarget"": [
...@@ -104,29 +105,37 @@ public class C ...@@ -104,29 +105,37 @@ public class C
}} }}
], ],
""fullMessage"": ""The field 'C.x' is never used"", ""fullMessage"": ""The field 'C.x' is never used"",
""isSuppressedInSource"": false,
""tags"": [
""Compiler"",
""Telemetry""
],
""properties"": {{ ""properties"": {{
""severity"": ""Warning"", ""severity"": ""Warning"",
""warningLevel"": ""3"", ""warningLevel"": ""3"",
""defaultSeverity"": ""Warning"", ""defaultSeverity"": ""Warning"",
""title"": ""Field is never used"", ""title"": ""Field is never used"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""False"",
""customTags"": ""Compiler;Telemetry""
}} }}
}}, }},
{{ {{
""ruleId"": ""CS5001"", ""ruleId"": ""CS5001"",
""kind"": ""error"",
""locations"": [ ""locations"": [
], ],
""fullMessage"": ""Program does not contain a static 'Main' method suitable for an entry point"", ""fullMessage"": ""Program does not contain a static 'Main' method suitable for an entry point"",
""isSuppressedInSource"": false,
""tags"": [
""Compiler"",
""Telemetry"",
""NotConfigurable""
],
""properties"": {{ ""properties"": {{
""severity"": ""Error"", ""severity"": ""Error"",
""defaultSeverity"": ""Error"", ""defaultSeverity"": ""Error"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""False"",
""customTags"": ""Compiler;Telemetry;NotConfigurable""
}} }}
}} }}
] ]
...@@ -171,9 +180,10 @@ public class C ...@@ -171,9 +180,10 @@ public class C
var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd);
var expectedIssues = string.Format(@" var expectedIssues = string.Format(@"
""issues"": [ ""results"": [
{{ {{
""ruleId"": ""CS0169"", ""ruleId"": ""CS0169"",
""kind"": ""warning"",
""locations"": [ ""locations"": [
{{ {{
""analysisTarget"": [ ""analysisTarget"": [
...@@ -190,29 +200,37 @@ public class C ...@@ -190,29 +200,37 @@ public class C
}} }}
], ],
""fullMessage"": ""The field 'C.x' is never used"", ""fullMessage"": ""The field 'C.x' is never used"",
""isSuppressedInSource"": true,
""tags"": [
""Compiler"",
""Telemetry""
],
""properties"": {{ ""properties"": {{
""severity"": ""Warning"", ""severity"": ""Warning"",
""warningLevel"": ""3"", ""warningLevel"": ""3"",
""defaultSeverity"": ""Warning"", ""defaultSeverity"": ""Warning"",
""title"": ""Field is never used"", ""title"": ""Field is never used"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""True"",
""customTags"": ""Compiler;Telemetry""
}} }}
}}, }},
{{ {{
""ruleId"": ""CS5001"", ""ruleId"": ""CS5001"",
""kind"": ""error"",
""locations"": [ ""locations"": [
], ],
""fullMessage"": ""Program does not contain a static 'Main' method suitable for an entry point"", ""fullMessage"": ""Program does not contain a static 'Main' method suitable for an entry point"",
""isSuppressedInSource"": false,
""tags"": [
""Compiler"",
""Telemetry"",
""NotConfigurable""
],
""properties"": {{ ""properties"": {{
""severity"": ""Error"", ""severity"": ""Error"",
""defaultSeverity"": ""Error"", ""defaultSeverity"": ""Error"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""False"",
""customTags"": ""Compiler;Telemetry;NotConfigurable""
}} }}
}} }}
] ]
...@@ -255,7 +273,7 @@ public class C ...@@ -255,7 +273,7 @@ public class C
var actualOutput = File.ReadAllText(errorLogFile).Trim(); var actualOutput = File.ReadAllText(errorLogFile).Trim();
var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd); var expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd);
var expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogIssuesText(cmd.Compilation); var expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogResultsText(cmd.Compilation);
var expectedText = expectedHeader + expectedIssues; var expectedText = expectedHeader + expectedIssues;
Assert.Equal(expectedText, actualOutput); Assert.Equal(expectedText, actualOutput);
......
...@@ -21,7 +21,7 @@ namespace Microsoft.CodeAnalysis ...@@ -21,7 +21,7 @@ namespace Microsoft.CodeAnalysis
internal partial class ErrorLogger : IDisposable internal partial class ErrorLogger : IDisposable
{ {
// Internal for testing purposes. // Internal for testing purposes.
internal const string OutputFormatVersion = "0.1"; internal const string OutputFormatVersion = "0.4";
private readonly JsonWriter _writer; private readonly JsonWriter _writer;
...@@ -40,7 +40,7 @@ public ErrorLogger(Stream stream, string toolName, string toolFileVersion, Versi ...@@ -40,7 +40,7 @@ public ErrorLogger(Stream stream, string toolName, string toolFileVersion, Versi
WriteToolInfo(toolName, toolFileVersion, toolAssemblyVersion); WriteToolInfo(toolName, toolFileVersion, toolAssemblyVersion);
_writer.WriteArrayStart("issues"); _writer.WriteArrayStart("results");
} }
private void WriteToolInfo(string name, string fileVersion, Version assemblyVersion) private void WriteToolInfo(string name, string fileVersion, Version assemblyVersion)
...@@ -54,8 +54,9 @@ private void WriteToolInfo(string name, string fileVersion, Version assemblyVers ...@@ -54,8 +54,9 @@ private void WriteToolInfo(string name, string fileVersion, Version assemblyVers
internal void LogDiagnostic(Diagnostic diagnostic, CultureInfo culture) internal void LogDiagnostic(Diagnostic diagnostic, CultureInfo culture)
{ {
_writer.WriteObjectStart(); // issue _writer.WriteObjectStart(); // result
_writer.Write("ruleId", diagnostic.Id); _writer.Write("ruleId", diagnostic.Id);
_writer.Write("kind", GetKind(diagnostic.Severity));
WriteLocations(diagnostic.Location, diagnostic.AdditionalLocations); WriteLocations(diagnostic.Location, diagnostic.AdditionalLocations);
...@@ -76,9 +77,13 @@ internal void LogDiagnostic(Diagnostic diagnostic, CultureInfo culture) ...@@ -76,9 +77,13 @@ internal void LogDiagnostic(Diagnostic diagnostic, CultureInfo culture)
_writer.Write("fullMessage", description); _writer.Write("fullMessage", description);
} }
_writer.Write("isSuppressedInSource", diagnostic.IsSuppressed);
WriteTags(diagnostic);
WriteProperties(diagnostic, culture); WriteProperties(diagnostic, culture);
_writer.WriteObjectEnd(); // issue _writer.WriteObjectEnd(); // result
} }
private void WriteLocations(Location location, IReadOnlyList<Location> additionalLocations) private void WriteLocations(Location location, IReadOnlyList<Location> additionalLocations)
...@@ -143,6 +148,21 @@ private static string GetUri(SyntaxTree syntaxTree) ...@@ -143,6 +148,21 @@ private static string GetUri(SyntaxTree syntaxTree)
return uri.ToString(); return uri.ToString();
} }
private void WriteTags(Diagnostic diagnostic)
{
if (diagnostic.CustomTags.Count > 0)
{
_writer.WriteArrayStart("tags");
foreach (string tag in diagnostic.CustomTags)
{
_writer.Write(tag);
}
_writer.WriteArrayEnd();
}
}
private void WriteProperties(Diagnostic diagnostic, CultureInfo culture) private void WriteProperties(Diagnostic diagnostic, CultureInfo culture)
{ {
_writer.WriteObjectStart("properties"); _writer.WriteObjectStart("properties");
...@@ -172,13 +192,6 @@ private void WriteProperties(Diagnostic diagnostic, CultureInfo culture) ...@@ -172,13 +192,6 @@ private void WriteProperties(Diagnostic diagnostic, CultureInfo culture)
_writer.Write("isEnabledByDefault", diagnostic.IsEnabledByDefault.ToString()); _writer.Write("isEnabledByDefault", diagnostic.IsEnabledByDefault.ToString());
_writer.Write("isSuppressedInSource", diagnostic.IsSuppressed.ToString());
if (diagnostic.CustomTags.Count > 0)
{
_writer.Write("customTags", diagnostic.CustomTags.WhereNotNull().Join(";"));
}
foreach (var pair in diagnostic.Properties.OrderBy(x => x.Key, StringComparer.Ordinal)) foreach (var pair in diagnostic.Properties.OrderBy(x => x.Key, StringComparer.Ordinal))
{ {
_writer.Write("customProperties." + pair.Key, pair.Value); _writer.Write("customProperties." + pair.Key, pair.Value);
...@@ -187,10 +200,30 @@ private void WriteProperties(Diagnostic diagnostic, CultureInfo culture) ...@@ -187,10 +200,30 @@ private void WriteProperties(Diagnostic diagnostic, CultureInfo culture)
_writer.WriteObjectEnd(); // properties _writer.WriteObjectEnd(); // properties
} }
private static string GetKind(DiagnosticSeverity severity)
{
switch (severity)
{
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";
}
}
public void Dispose() public void Dispose()
{ {
_writer.WriteArrayEnd(); // issues _writer.WriteArrayEnd(); // results
_writer.WriteObjectEnd(); // single runLog _writer.WriteObjectEnd(); // runLog
_writer.WriteArrayEnd(); // runLogs _writer.WriteArrayEnd(); // runLogs
_writer.WriteObjectEnd(); // root _writer.WriteObjectEnd(); // root
_writer.Dispose(); _writer.Dispose();
......
...@@ -46,7 +46,7 @@ End Class ...@@ -46,7 +46,7 @@ End Class
Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd)
Dim expectedIssues = " Dim expectedIssues = "
""issues"": [ ""results"": [
] ]
} }
] ]
...@@ -91,9 +91,10 @@ End Class ...@@ -91,9 +91,10 @@ End Class
Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd)
Dim expectedIssues = String.Format(" Dim expectedIssues = String.Format("
""issues"": [ ""results"": [
{{ {{
""ruleId"": ""BC42024"", ""ruleId"": ""BC42024"",
""kind"": ""warning"",
""locations"": [ ""locations"": [
{{ {{
""analysisTarget"": [ ""analysisTarget"": [
...@@ -110,29 +111,37 @@ End Class ...@@ -110,29 +111,37 @@ End Class
}} }}
], ],
""fullMessage"": ""Unused local variable: 'x'."", ""fullMessage"": ""Unused local variable: 'x'."",
""isSuppressedInSource"": false,
""tags"": [
""Compiler"",
""Telemetry""
],
""properties"": {{ ""properties"": {{
""severity"": ""Warning"", ""severity"": ""Warning"",
""warningLevel"": ""1"", ""warningLevel"": ""1"",
""defaultSeverity"": ""Warning"", ""defaultSeverity"": ""Warning"",
""title"": ""Unused local variable"", ""title"": ""Unused local variable"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""False"",
""customTags"": ""Compiler;Telemetry""
}} }}
}}, }},
{{ {{
""ruleId"": ""BC30420"", ""ruleId"": ""BC30420"",
""kind"": ""error"",
""locations"": [ ""locations"": [
], ],
""fullMessage"": ""'Sub Main' was not found in '{1}'."", ""fullMessage"": ""'Sub Main' was not found in '{1}'."",
""isSuppressedInSource"": false,
""tags"": [
""Compiler"",
""Telemetry"",
""NotConfigurable""
],
""properties"": {{ ""properties"": {{
""severity"": ""Error"", ""severity"": ""Error"",
""defaultSeverity"": ""Error"", ""defaultSeverity"": ""Error"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""False"",
""customTags"": ""Compiler;Telemetry;NotConfigurable""
}} }}
}} }}
] ]
...@@ -182,9 +191,10 @@ End Class ...@@ -182,9 +191,10 @@ End Class
Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd)
Dim expectedIssues = String.Format(" Dim expectedIssues = String.Format("
""issues"": [ ""results"": [
{{ {{
""ruleId"": ""BC42024"", ""ruleId"": ""BC42024"",
""kind"": ""warning"",
""locations"": [ ""locations"": [
{{ {{
""analysisTarget"": [ ""analysisTarget"": [
...@@ -201,29 +211,37 @@ End Class ...@@ -201,29 +211,37 @@ End Class
}} }}
], ],
""fullMessage"": ""Unused local variable: 'x'."", ""fullMessage"": ""Unused local variable: 'x'."",
""isSuppressedInSource"": true,
""tags"": [
""Compiler"",
""Telemetry""
],
""properties"": {{ ""properties"": {{
""severity"": ""Warning"", ""severity"": ""Warning"",
""warningLevel"": ""1"", ""warningLevel"": ""1"",
""defaultSeverity"": ""Warning"", ""defaultSeverity"": ""Warning"",
""title"": ""Unused local variable"", ""title"": ""Unused local variable"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""True"",
""customTags"": ""Compiler;Telemetry""
}} }}
}}, }},
{{ {{
""ruleId"": ""BC30420"", ""ruleId"": ""BC30420"",
""kind"": ""error"",
""locations"": [ ""locations"": [
], ],
""fullMessage"": ""'Sub Main' was not found in '{1}'."", ""fullMessage"": ""'Sub Main' was not found in '{1}'."",
""isSuppressedInSource"": false,
""tags"": [
""Compiler"",
""Telemetry"",
""NotConfigurable""
],
""properties"": {{ ""properties"": {{
""severity"": ""Error"", ""severity"": ""Error"",
""defaultSeverity"": ""Error"", ""defaultSeverity"": ""Error"",
""category"": ""Compiler"", ""category"": ""Compiler"",
""isEnabledByDefault"": ""True"", ""isEnabledByDefault"": ""True""
""isSuppressedInSource"": ""False"",
""customTags"": ""Compiler;Telemetry;NotConfigurable""
}} }}
}} }}
] ]
...@@ -271,7 +289,7 @@ End Class ...@@ -271,7 +289,7 @@ End Class
Dim actualOutput = File.ReadAllText(errorLogFile).Trim() Dim actualOutput = File.ReadAllText(errorLogFile).Trim()
Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd) Dim expectedHeader = GetExpectedErrorLogHeader(actualOutput, cmd)
Dim expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogIssuesText(cmd.Compilation) Dim expectedIssues = AnalyzerForErrorLogTest.GetExpectedErrorLogResultsText(cmd.Compilation)
Dim expectedText = expectedHeader + expectedIssues Dim expectedText = expectedHeader + expectedIssues
Assert.Equal(expectedText, actualOutput) Assert.Equal(expectedText, actualOutput)
......
...@@ -77,7 +77,7 @@ private static string GetExpectedPropertiesMapText() ...@@ -77,7 +77,7 @@ private static string GetExpectedPropertiesMapText()
return expectedText; return expectedText;
} }
public static string GetExpectedErrorLogIssuesText(Compilation compilation) public static string GetExpectedErrorLogResultsText(Compilation compilation)
{ {
var tree = compilation.SyntaxTrees.First(); var tree = compilation.SyntaxTrees.First();
var root = tree.GetRoot(); var root = tree.GetRoot();
...@@ -85,9 +85,10 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation) ...@@ -85,9 +85,10 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation)
var filePath = GetEscapedUriForPath(tree.FilePath); var filePath = GetEscapedUriForPath(tree.FilePath);
return @" return @"
""issues"": [ ""results"": [
{ {
""ruleId"": """ + Descriptor1.Id + @""", ""ruleId"": """ + Descriptor1.Id + @""",
""kind"": """ + (Descriptor1.DefaultSeverity == DiagnosticSeverity.Error ? "error" : "warning") + @""",
""locations"": [ ""locations"": [
{ {
""analysisTarget"": [ ""analysisTarget"": [
...@@ -105,6 +106,10 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation) ...@@ -105,6 +106,10 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation)
], ],
""shortMessage"": """ + Descriptor1.MessageFormat + @""", ""shortMessage"": """ + Descriptor1.MessageFormat + @""",
""fullMessage"": """ + Descriptor1.Description + @""", ""fullMessage"": """ + Descriptor1.Description + @""",
""isSuppressedInSource"": false,
""tags"": [
" + String.Join("," + Environment.NewLine + " ", Descriptor1.CustomTags.Select(s => $"\"{s}\"")) + @"
],
""properties"": { ""properties"": {
""severity"": """ + Descriptor1.DefaultSeverity + @""", ""severity"": """ + Descriptor1.DefaultSeverity + @""",
""warningLevel"": ""1"", ""warningLevel"": ""1"",
...@@ -112,27 +117,28 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation) ...@@ -112,27 +117,28 @@ public static string GetExpectedErrorLogIssuesText(Compilation compilation)
""title"": """ + Descriptor1.Title + @""", ""title"": """ + Descriptor1.Title + @""",
""category"": """ + Descriptor1.Category + @""", ""category"": """ + Descriptor1.Category + @""",
""helpLink"": """ + Descriptor1.HelpLinkUri + @""", ""helpLink"": """ + Descriptor1.HelpLinkUri + @""",
""isEnabledByDefault"": """ + Descriptor1.IsEnabledByDefault + @""", ""isEnabledByDefault"": """ + Descriptor1.IsEnabledByDefault + @"""" +
""isSuppressedInSource"": ""False"",
""customTags"": """ + Descriptor1.CustomTags.Join(";") + @"""" +
GetExpectedPropertiesMapText() + @" GetExpectedPropertiesMapText() + @"
} }
}, },
{ {
""ruleId"": """ + Descriptor2.Id + @""", ""ruleId"": """ + Descriptor2.Id + @""",
""kind"": """ + (Descriptor2.DefaultSeverity == DiagnosticSeverity.Error ? "error" : "warning") + @""",
""locations"": [ ""locations"": [
], ],
""shortMessage"": """ + Descriptor2.MessageFormat + @""", ""shortMessage"": """ + Descriptor2.MessageFormat + @""",
""fullMessage"": """ + Descriptor2.Description + @""", ""fullMessage"": """ + Descriptor2.Description + @""",
""isSuppressedInSource"": false,
""tags"": [
" + String.Join("," + Environment.NewLine + " ", Descriptor2.CustomTags.Select(s => $"\"{s}\"")) + @"
],
""properties"": { ""properties"": {
""severity"": """ + Descriptor2.DefaultSeverity + @""", ""severity"": """ + Descriptor2.DefaultSeverity + @""",
""defaultSeverity"": """ + Descriptor2.DefaultSeverity + @""", ""defaultSeverity"": """ + Descriptor2.DefaultSeverity + @""",
""title"": """ + Descriptor2.Title + @""", ""title"": """ + Descriptor2.Title + @""",
""category"": """ + Descriptor2.Category + @""", ""category"": """ + Descriptor2.Category + @""",
""helpLink"": """ + Descriptor2.HelpLinkUri + @""", ""helpLink"": """ + Descriptor2.HelpLinkUri + @""",
""isEnabledByDefault"": """ + Descriptor2.IsEnabledByDefault + @""", ""isEnabledByDefault"": """ + Descriptor2.IsEnabledByDefault + @"""" +
""isSuppressedInSource"": ""False"",
""customTags"": """ + Descriptor2.CustomTags.Join(";") + @"""" +
GetExpectedPropertiesMapText() + @" GetExpectedPropertiesMapText() + @"
} }
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册