Skip to content

Commit 454395c

Browse files
authored
Fixed analyzer diagnostic message. (#28)
* Sadly default verification doesn't consider the arguments of a message - Analyzer for nullable + required properties was using the wrong message argument. * Added common verifier function for validating the message arguments * Added custom verification to include the common verifier and the help URI analyzer
1 parent 9746da1 commit 454395c

5 files changed

Lines changed: 111 additions & 26 deletions

File tree

src/Ubiquity.NET.CommandLine.SrcGen.UT/CommandAnalyzerTests.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public async Task Option_attribute_without_command_triggers_diagnostic( TestRunt
2929
analyzerTest.ExpectedDiagnostics.AddRange(
3030
[
3131
new DiagnosticResult("UNC001", DiagnosticSeverity.Error)
32-
.WithSpan(9, 6, 9, 93)
33-
.WithArguments("OptionAttribute"),
32+
.WithArguments("OptionAttribute")
33+
.WithSpan(9, 6, 9, 93),
3434
]
3535
);
3636

@@ -75,12 +75,12 @@ public async Task FolderValidation_attribute_without_command_triggers_diagnostic
7575
analyzerTest.ExpectedDiagnostics.AddRange(
7676
[
7777
new DiagnosticResult("UNC001", DiagnosticSeverity.Error)
78-
.WithSpan(11, 6, 11, 55)
79-
.WithArguments("FolderValidationAttribute"),
78+
.WithArguments("FolderValidationAttribute")
79+
.WithSpan(11, 6, 11, 55),
8080

8181
new DiagnosticResult("UNC002", DiagnosticSeverity.Error)
82-
.WithSpan(11, 6, 11, 55)
83-
.WithArguments("FolderValidationAttribute"),
82+
.WithArguments("FolderValidationAttribute")
83+
.WithSpan(11, 6, 11, 55),
8484
]
8585
);
8686

@@ -110,6 +110,7 @@ public async Task FileValidation_with_wrong_type_produces_UNC002( TestRuntime te
110110
analyzerTest.ExpectedDiagnostics.AddRange(
111111
[
112112
new DiagnosticResult("UNC003", DiagnosticSeverity.Error)
113+
.WithArguments("FileValidationAttribute", "System.IO.FileInfo")
113114
.WithSpan(9, 6, 9, 51),
114115
]
115116
);
@@ -129,8 +130,8 @@ public async Task Required_nullable_types_produce_diagnostic( TestRuntime testRu
129130
analyzerTest.ExpectedDiagnostics.AddRange(
130131
[
131132
new DiagnosticResult("UNC004", DiagnosticSeverity.Warning)
132-
.WithSpan(9, 6, 9, 127)
133-
.WithArguments("bool?", "OptionAttribute"),
133+
.WithArguments("bool?", "Thing1")
134+
.WithSpan(9, 6, 9, 127),
134135
]
135136
);
136137

@@ -165,15 +166,29 @@ private AnalyzerTest<MsTestVerifier> CreateTestRunner( SourceText source, TestRu
165166
// Allow ALL diagnostics for testing, input source should contain valid C# code
166167
// but might otherwise trigger the tested analyzer.
167168
CompilerDiagnostics = CompilerDiagnostics.All,
169+
170+
// Add custom verification to handle cases not covered in default verifications
171+
DiagnosticVerifier = DiagnosticVerifier,
168172
};
169173
}
170174

171-
// Sadly, at this time the test infrastructure doesn't provide support for
172-
// testing the help URI
173-
//private static string FormatHelpUri( string id )
174-
//{
175-
// return $"https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/{id}.html";
176-
//}
175+
/// <summary>Custom diagnostic verifier</summary>
176+
/// <param name="diagnostic">diagnostic to verify (actual)</param>
177+
/// <param name="result">Diagnostic result containing the expected results</param>
178+
/// <param name="verifier">Implementation of <see cref="IVerifier"/> to use for verification</param>
179+
/// <seealso href="https://github.com/dotnet/roslyn-sdk/issues/1246"/>
180+
private void DiagnosticVerifier( Diagnostic diagnostic, DiagnosticResult result, IVerifier verifier )
181+
{
182+
DiagnosticVerifiers.VerifyMessageArguments( diagnostic, result, verifier );
183+
184+
// verify the help URI matches the expected form
185+
verifier.EqualOrDiff( diagnostic.Descriptor.HelpLinkUri, FormatHelpUri( diagnostic.Id ) );
186+
}
187+
188+
private static string FormatHelpUri( string id )
189+
{
190+
return $"https://ubiquitydotnet.github.io/Ubiquity.NET.Utils/CommandLine/diagnostics/{id}.html";
191+
}
177192

178193
private static SourceText GetSourceText( params string[] nameParts )
179194
{

src/Ubiquity.NET.CommandLine.SrcGen/CommandLineAnalyzer.cs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ EquatableAttributeDataCollection attributes
115115
VerifyCommandAttribute( context, attribLoc, Constants.OptionAttribute.SimpleName );
116116

117117
EquatableAttributeData attribute = attributes[Constants.OptionAttribute];
118-
VerifyNotNullableRequired( context, symbol, attribute, attribLoc, Constants.OptionAttribute.SimpleName );
118+
VerifyNotNullableRequired( context, symbol, attribute, attribLoc );
119119

120120
// Additional validations...
121121
}
@@ -131,7 +131,7 @@ EquatableAttributeDataCollection attribs
131131
VerifyCommandAttribute( context, attribLoc, Constants.FileValidationAttribute.SimpleName );
132132

133133
// Verify an Option property (or maybe an argument attribute once supported) exists
134-
VerifyHasConstrainedAttribute( context, attribLoc, attribs );
134+
VerifyHasConstrainedAttribute( context, attribLoc, attribs, Constants.FileValidationAttribute.SimpleName );
135135

136136
// Verify type of the property is System.IO.FileInfo.
137137
VerifyRequiredPropertyType( context, symbol, Constants.FileInfo, attribLoc, Constants.FileValidationAttribute.SimpleName );
@@ -150,9 +150,9 @@ EquatableAttributeDataCollection attribs
150150
VerifyCommandAttribute( context, attribLoc, Constants.FolderValidationAttribute.SimpleName );
151151

152152
// Verify an Option property (or maybe an argument attribute once supported) exists
153-
VerifyHasConstrainedAttribute( context, attribLoc, attribs );
153+
VerifyHasConstrainedAttribute( context, attribLoc, attribs, Constants.FolderValidationAttribute.SimpleName );
154154

155-
// Verify type of the property is System.IO.FileInfo.
155+
// Verify type of the property is System.IO.DirectoryInfo.
156156
VerifyRequiredPropertyType( context, symbol, Constants.DirectoryInfo, attribLoc, Constants.FolderValidationAttribute.SimpleName );
157157

158158
// Additional validations...
@@ -183,18 +183,26 @@ private static void VerifyCommandAttribute( SymbolAnalysisContext context, Locat
183183
/// <param name="context">Context to use for reporting diagnostics</param>
184184
/// <param name="attribLoc">Location to use for any diagnostics reported</param>
185185
/// <param name="attribs">Set of attributes to check</param>
186+
/// <param name="typeConstraintName">Name of the attribute that requires a constraint (For diagnostic message)</param>
186187
/// <remarks>
187188
/// The <paramref name="attribLoc"/> is used to report diagnostics that normally references the attribute
188189
/// that is missing the constrained attribute. (ex. The `FileValidation` or `FolderValidation` that does NOT
189190
/// have an `OptionAttribute` that it validates.
190191
/// </remarks>
191-
private static void VerifyHasConstrainedAttribute( SymbolAnalysisContext context, Location? attribLoc, EquatableAttributeDataCollection attribs )
192+
private static void VerifyHasConstrainedAttribute(
193+
SymbolAnalysisContext context,
194+
Location? attribLoc,
195+
EquatableAttributeDataCollection attribs,
196+
string typeConstraintName
197+
)
192198
{
193-
// Verify an Option property (or maybe an argument attribute once supported) exists
199+
// Verify an Option property
194200
if(!attribs.TryGetValue( Constants.OptionAttribute, out _ ))
195201
{
196-
ReportDiagnostic( context, Diagnostics.MissingConstraintAttribute, attribLoc, Constants.FileValidationAttribute.SimpleName );
202+
ReportDiagnostic( context, Diagnostics.MissingConstraintAttribute, attribLoc, typeConstraintName );
197203
}
204+
205+
// TODO: validate an Argument attribute or Option attribute
198206
}
199207

200208
/// <summary>Verifies a property has an expected type</summary>
@@ -226,17 +234,15 @@ private static void VerifyNotNullableRequired(
226234
SymbolAnalysisContext context,
227235
IPropertySymbol property,
228236
EquatableAttributeData attribute,
229-
Location? attribLoc,
230-
string simpleName
231-
)
237+
Location? attribLoc )
232238
{
233239
if(attribute.NamedArguments.TryGetValue( Constants.CommonAttributeNamedArgs.Required, out StructurallyEquatableTypedConstant tc ))
234240
{
235241
bool isRequired = !tc.IsNull && (bool)tc.Value!;
236242
NamespaceQualifiedTypeName propType = property.Type.GetNamespaceQualifiedName();
237243
if(isRequired && propType.IsNullable)
238244
{
239-
ReportDiagnostic( context, Diagnostics.RequiredNullableType, attribLoc, $"{propType:A}", simpleName );
245+
ReportDiagnostic( context, Diagnostics.RequiredNullableType, attribLoc, $"{propType:A}", property.Name );
240246
}
241247
}
242248
}

src/Ubiquity.NET.CommandLine.SrcGen/Diagnostics.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal static ImmutableArray<DiagnosticDescriptor> CommandLineAnalyzerDiagnost
3232
RequiredNullableType,
3333
];
3434

35+
// Exception message is: '{0}'
3536
internal static readonly DiagnosticDescriptor InternalError = new(
3637
id: IDs.InternalError,
3738
title: Localized(nameof(Resources.InternalError_Title)),
@@ -44,6 +45,7 @@ internal static ImmutableArray<DiagnosticDescriptor> CommandLineAnalyzerDiagnost
4445
WellKnownDiagnosticTags.NotConfigurable
4546
);
4647

48+
// Property attribute '{0}' is only allowed on a property in a type attributed with a command attribute. This use will be ignored by the generator.
4749
internal static readonly DiagnosticDescriptor MissingCommandAttribute = new(
4850
id: IDs.MissingCommandAttribute,
4951
title: Localized(nameof(Resources.MissingCommandAttribute_Title)),
@@ -57,6 +59,7 @@ internal static ImmutableArray<DiagnosticDescriptor> CommandLineAnalyzerDiagnost
5759
WellKnownDiagnosticTags.Compiler
5860
);
5961

62+
// Property attribute '{0}' is not allowed on a property independent of a qualifying attribute such as OptionAttribute.
6063
internal static readonly DiagnosticDescriptor MissingConstraintAttribute = new(
6164
id: IDs.MissingConstraintAttribute,
6265
title: Localized(nameof(Resources.MissingConstraintAttribute_Title)),
@@ -69,6 +72,7 @@ internal static ImmutableArray<DiagnosticDescriptor> CommandLineAnalyzerDiagnost
6972
WellKnownDiagnosticTags.Compiler
7073
);
7174

75+
// Property attribute '{0}' requires a property of type '{1}'.
7276
internal static readonly DiagnosticDescriptor IncorrectPropertyType = new(
7377
id: IDs.IncorrectPropertyType,
7478
title: Localized(nameof(Resources.IncorrectPropertyType_Title)),

src/Ubiquity.NET.SourceGenerator.Test.Utils/CSharp/SourceAnalyzerTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ namespace Ubiquity.NET.SourceGenerator.Test.Utils.CSharp
1313
/// <summary>Source analyzer tests for C# that allows specification of the language</summary>
1414
/// <typeparam name="TAnalyzer">Analyzer type</typeparam>
1515
/// <typeparam name="TVerifier">Verifier type</typeparam>
16-
public class SourceAnalyzerTest<TAnalyzer, TVerifier> : AnalyzerTest<TVerifier>
16+
public class SourceAnalyzerTest<TAnalyzer, TVerifier>
17+
: AnalyzerTest<TVerifier>
1718
where TAnalyzer : DiagnosticAnalyzer, new()
1819
where TVerifier : IVerifier, new()
1920
{
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Copyright (c) Ubiquity.NET Contributors. All rights reserved.
2+
// Licensed under the Apache-2.0 WITH LLVM-exception license. See the LICENSE.md file in the project root for full license information.
3+
4+
using System.Diagnostics;
5+
using System.Globalization;
6+
using System.Text;
7+
8+
using Microsoft.CodeAnalysis;
9+
using Microsoft.CodeAnalysis.Testing;
10+
11+
namespace Ubiquity.NET.SourceGenerator.Test.Utils
12+
{
13+
/// <summary>Utility class to provide additional verification of a <see cref="Diagnostic"/></summary>
14+
/// <remarks>
15+
/// <see cref="AnalyzerTest{TVerifier}.DiagnosticVerifier"/> uses only a single delegate AND the default
16+
/// handling of a test doesn't cover some common scenarios. This class provides those in a form that is
17+
/// designed to make compossible verifiers as needed.
18+
/// </remarks>
19+
public static class DiagnosticVerifiers
20+
{
21+
/// <summary>Custom diagnostic verifier to validate the message arguments of a <see cref="Diagnostic"/></summary>
22+
/// <param name="diagnostic">diagnostic to verify (actual)</param>
23+
/// <param name="result">Diagnostic result containing the expected results</param>
24+
/// <param name="verifier">Implementation of <see cref="IVerifier"/> to use for verification</param>
25+
/// <remarks>
26+
/// Default verification provided by <see cref="AnalyzerTest{TVerifier}"/> use an undocumented set
27+
/// of heuristics to verify a given <see cref="Diagnostic"/> is "roughly equivalent" to an expected
28+
/// <see cref="DiagnosticResult"/>. Unfortunately, the heuristics used are not documented and ultimately
29+
/// don't include verification of the message args (which a diagnostic producer might get wrong). This
30+
/// method will verify the arguments by formatting a message using the expected arguments and comparing
31+
/// that to the message for the <paramref name="diagnostic"/>.
32+
/// </remarks>
33+
/// <seealso href="https://github.com/dotnet/roslyn-sdk/issues/1246"/>
34+
public static void VerifyMessageArguments( Diagnostic diagnostic, DiagnosticResult result, IVerifier verifier )
35+
{
36+
var parsedFormat = CompositeFormat.Parse(diagnostic.Descriptor.MessageFormat.ToString());
37+
int expectedMessageCount = result.MessageArguments?.Length ?? 0;
38+
int actualMessageCount = parsedFormat.MinimumArgumentCount;
39+
40+
verifier.Equal(
41+
parsedFormat.MinimumArgumentCount,
42+
expectedMessageCount,
43+
$"Missing test arguments; The number of expected arguments ({expectedMessageCount}) does not match the actual message {actualMessageCount}. [This is a debug only verification]"
44+
);
45+
46+
// only test diagnostic if there are expected message args to validate
47+
// There's no direct way to access the arguments of a diagnostic. (It's there but "internal" and
48+
// therefore can not be relied upon). So, get the message format from the descriptor and format a message
49+
// with the expected arguments. Then test the result of that against the message reported by the diagnostic.
50+
// This will ultimately test that the arguments match expectations.
51+
if(expectedMessageCount > 0 && parsedFormat.MinimumArgumentCount > 0 && result.MessageArguments is not null)
52+
{
53+
string expectedMessage = string.Format(CultureInfo.CurrentCulture, parsedFormat.Format, result.MessageArguments!);
54+
string actualMessage = diagnostic.GetMessage( CultureInfo.CurrentCulture );
55+
verifier.Equal( expectedMessage, actualMessage, "Message arguments should be the same" );
56+
}
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)