Skip to content

Commit a93e42c

Browse files
committed
Detect positional attribute argument name via constructor
1 parent 831a8e9 commit a93e42c

File tree

3 files changed

+114
-10
lines changed

3 files changed

+114
-10
lines changed

src/DendroDocs.Tool/Analyzers/SourceAnalyzer.cs

+41-8
Original file line numberDiff line numberDiff line change
@@ -268,18 +268,12 @@ private void ExtractAttributes(SyntaxList<AttributeListSyntax> attributes, List<
268268
{
269269
foreach (var attribute in attributes.SelectMany(a => a.Attributes))
270270
{
271-
var attributeDescription = new AttributeDescription(semanticModel.GetTypeDisplayString(attribute), attribute.Name.ToString());
271+
var attributeDescription = this.CreateAttributeDescription(attribute);
272272
attributeDescriptions.Add(attributeDescription);
273273

274274
if (attribute.ArgumentList != null)
275275
{
276-
foreach (var argument in attribute.ArgumentList.Arguments)
277-
{
278-
var value = argument.Expression!.ResolveValue(semanticModel);
279-
280-
var argumentDescription = new AttributeArgumentDescription(argument.NameEquals?.Name.ToString() ?? argument.Expression.ResolveValue(semanticModel), semanticModel.GetTypeDisplayString(argument.Expression!), value);
281-
attributeDescription.Arguments.Add(argumentDescription);
282-
}
276+
this.AddAttributeArguments(attribute, attributeDescription);
283277
}
284278
}
285279
}
@@ -314,4 +308,43 @@ private static Modifier ParseModifiers(SyntaxTokenList modifiers)
314308
{
315309
return (Modifier)modifiers.Select(m => Enum.TryParse(typeof(Modifier), m.ValueText, true, out var value) ? (int)value : 0).Sum();
316310
}
311+
312+
private AttributeDescription CreateAttributeDescription(AttributeSyntax attribute)
313+
{
314+
var typeDisplayString = semanticModel.GetTypeDisplayString(attribute);
315+
var attributeName = attribute.Name.ToString();
316+
317+
return new AttributeDescription(typeDisplayString, attributeName);
318+
}
319+
320+
private void AddAttributeArguments(AttributeSyntax attribute, AttributeDescription attributeDescription)
321+
{
322+
var argumentType = semanticModel.GetTypeInfo(attribute).Type;
323+
var ctor = FindMatchingArgumentConstructor(argumentType, attribute.ArgumentList!.Arguments.Count);
324+
325+
for (int i = 0; i < attribute.ArgumentList.Arguments.Count; i++)
326+
{
327+
var argument = attribute.ArgumentList.Arguments[i];
328+
var argumentDescription = this.CreateArgumentDescription(argument, ctor, i);
329+
330+
attributeDescription.Arguments.Add(argumentDescription);
331+
}
332+
}
333+
334+
private static IMethodSymbol? FindMatchingArgumentConstructor(ITypeSymbol? argumentType, int argumentCount)
335+
{
336+
return argumentType?
337+
.GetMembers(".ctor")
338+
.OfType<IMethodSymbol>()
339+
.FirstOrDefault(c => c.Parameters.Length == argumentCount);
340+
}
341+
342+
private AttributeArgumentDescription CreateArgumentDescription(AttributeArgumentSyntax argument, IMethodSymbol? ctor, int index)
343+
{
344+
var name = argument.NameEquals?.Name.ToString() ?? ctor?.Parameters[index].Name ?? argument.Expression.ResolveValue(semanticModel);
345+
var typeDisplayString = semanticModel.GetTypeDisplayString(argument.Expression!);
346+
var value = argument.Expression!.ResolveValue(semanticModel);
347+
348+
return new AttributeArgumentDescription(name, typeDisplayString, value);
349+
}
317350
}

tests/DendroDocs.Tool.Tests/AttributeDeclarationTests.cs

+72-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,54 @@ class Test
4040
types[0].Attributes.Should().HaveCount(1);
4141
types[0].Attributes[0].Should().NotBeNull();
4242
types[0].Attributes[0].Name.Should().Be("System.Obsolete");
43+
types[0].Attributes[0].Arguments.Should().BeEmpty();
44+
}
45+
46+
[TestMethod]
47+
public void ClassWithAttribute_Should_HaveAttributeWithPositionalArgumentInCollection()
48+
{
49+
// Assign
50+
var source =
51+
"""
52+
[System.Obsolete("Message")]
53+
class Test
54+
{
55+
}
56+
""";
57+
58+
// Act
59+
var types = TestHelper.VisitSyntaxTree(source);
60+
61+
// Assert
62+
types[0].Attributes[0].Arguments.Should().HaveCount(1);
63+
types[0].Attributes[0].Arguments[0].Should().NotBeNull();
64+
types[0].Attributes[0].Arguments[0].Name.Should().Be("message");
65+
types[0].Attributes[0].Arguments[0].Value.Should().Be("Message");
66+
}
67+
68+
[TestMethod]
69+
public void ClassWithAttribute_Should_HaveAttributeWithMultiplePositionalArgumentsInCollection()
70+
{
71+
// Assign
72+
var source =
73+
"""
74+
[System.Obsolete("Message", true)]
75+
class Test
76+
{
77+
}
78+
""";
79+
80+
// Act
81+
var types = TestHelper.VisitSyntaxTree(source);
82+
83+
// Assert
84+
types[0].Attributes[0].Arguments.Should().HaveCount(2);
85+
types[0].Attributes[0].Arguments[0].Name.Should().Be("message");
86+
types[0].Attributes[0].Arguments[0].Type.Should().Be("string");
87+
types[0].Attributes[0].Arguments[0].Value.Should().Be("Message");
88+
types[0].Attributes[0].Arguments[1].Name.Should().Be("error");
89+
types[0].Attributes[0].Arguments[1].Type.Should().Be("bool");
90+
types[0].Attributes[0].Arguments[1].Value.Should().Be("true");
4391
}
4492

4593
[TestMethod]
@@ -63,7 +111,30 @@ class Test
63111
types[0].Attributes[0].Arguments[0].Name.Should().Be("DiagnosticId");
64112
types[0].Attributes[0].Arguments[0].Value.Should().Be("ID");
65113
}
66-
114+
115+
[TestMethod]
116+
public void ClassWithAttribute_Should_HaveAttributeWithMixedArgumentInCollection()
117+
{
118+
// Assign
119+
var source =
120+
"""
121+
[System.Obsolete("Message", DiagnosticId = "ID")]
122+
class Test
123+
{
124+
}
125+
""";
126+
127+
// Act
128+
var types = TestHelper.VisitSyntaxTree(source);
129+
130+
// Assert
131+
types[0].Attributes[0].Arguments.Should().HaveCount(2);
132+
types[0].Attributes[0].Arguments[0].Name.Should().Be("message");
133+
types[0].Attributes[0].Arguments[0].Value.Should().Be("Message");
134+
types[0].Attributes[0].Arguments[1].Name.Should().Be("DiagnosticId");
135+
types[0].Attributes[0].Arguments[1].Value.Should().Be("ID");
136+
}
137+
67138
[TestMethod]
68139
public void EnumWithoutAttributes_Should_HaveEmptyAttributeCollection()
69140
{

tests/DendroDocs.Tool.Tests/SerializationTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class Test {
118118
var result = JsonConvert.SerializeObject(types, JsonDefaults.SerializerSettings());
119119

120120
// Assert
121-
result.Should().Match(@"[{""FullName"":""Test"",""Attributes"":[{""Type"":""System.ObsoleteAttribute"",""Name"":""System.Obsolete"",""Arguments"":[{""Name"":""Reason"",""Type"":""string"",""Value"":""Reason""}]}]}]");
121+
result.Should().Match(@"[{""FullName"":""Test"",""Attributes"":[{""Type"":""System.ObsoleteAttribute"",""Name"":""System.Obsolete"",""Arguments"":[{""Name"":""message"",""Type"":""string"",""Value"":""Reason""}]}]}]");
122122
}
123123
}
124124
}

0 commit comments

Comments
 (0)