Skip to content

Commit 8e75139

Browse files
committed
Update code fixer to support multiple attributes
1 parent df5ba86 commit 8e75139

File tree

1 file changed

+99
-31
lines changed

1 file changed

+99
-31
lines changed

src/CommunityToolkit.Mvvm.CodeFixers/UsePartialPropertyForObservablePropertyCodeFixer.cs

Lines changed: 99 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,6 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
100100
if (root!.FindNode(diagnosticSpan).FirstAncestorOrSelf<FieldDeclarationSyntax>() is { Declaration.Variables: [{ Identifier.Text: string identifierName }] } fieldDeclaration &&
101101
identifierName == fieldName)
102102
{
103-
// We only support fields with up to one attribute per attribute list.
104-
// This is so we can easily check one attribute when updating targets.
105-
if (fieldDeclaration.AttributeLists.Any(static list => list.Attributes.Count > 1))
106-
{
107-
return;
108-
}
109-
110103
// Register the code fix to update the class declaration to inherit from ObservableObject instead
111104
context.RegisterCodeFix(
112105
CodeAction.Create(
@@ -194,33 +187,108 @@ private static async Task<Document> ConvertToPartialProperty(
194187
continue;
195188
}
196189

197-
// Make sure we can retrieve the symbol for the attribute type.
198-
// We are guaranteed to always find a single attribute in the list.
199-
if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol))
190+
if (attributeListSyntax.Attributes.Count == 1)
200191
{
201-
return document;
202-
}
203-
204-
// Case 3
205-
if (toolkitTypeSymbols.ContainsValue(attributeSymbol))
206-
{
207-
propertyAttributes[i] = attributeListSyntax.WithTarget(null);
208-
209-
continue;
192+
// Make sure we can retrieve the symbol for the attribute type
193+
if (!semanticModel.GetSymbolInfo(attributeListSyntax.Attributes[0], cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol))
194+
{
195+
return document;
196+
}
197+
198+
// Case 3
199+
if (toolkitTypeSymbols.ContainsValue(attributeSymbol))
200+
{
201+
propertyAttributes[i] = attributeListSyntax.WithTarget(null);
202+
203+
continue;
204+
}
205+
206+
// Case 4
207+
if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol))
208+
{
209+
continue;
210+
}
211+
212+
// Case 5
213+
if (attributeListSyntax.Target is null)
214+
{
215+
propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword)));
216+
217+
continue;
218+
}
210219
}
211-
212-
// Case 4
213-
if (annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol))
214-
{
215-
continue;
216-
}
217-
218-
// Case 5
219-
if (attributeListSyntax.Target is null)
220+
else
220221
{
221-
propertyAttributes[i] = attributeListSyntax.WithTarget(AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword)));
222-
223-
continue;
222+
// If we have multiple attributes in the current list, we need additional logic here.
223+
// We could have any number of attributes here, so we split them into three buckets:
224+
// - MVVM Toolkit attributes: these should be moved over with no target
225+
// - Data annotation or validation attributes: these should be moved over with the same target
226+
// - Any other attributes: these should be moved over with the 'field' target
227+
List<AttributeSyntax> mvvmToolkitAttributes = [];
228+
List<AttributeSyntax> annotationOrValidationAttributes = [];
229+
List<AttributeSyntax> fieldAttributes = [];
230+
231+
foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes)
232+
{
233+
// Like for the single attribute case, make sure we can get the symbol for the attribute
234+
if (!semanticModel.GetSymbolInfo(attributeSyntax, cancellationToken).TryGetAttributeTypeSymbol(out INamedTypeSymbol? attributeSymbol))
235+
{
236+
return document;
237+
}
238+
239+
bool isAnnotationOrValidationAttribute = annotationTypeSymbols.ContainsValue(attributeSymbol) || attributeSymbol.InheritsFromType(validationAttributeSymbol);
240+
241+
// Split the attributes into the buckets. Note that we have a special rule for annotation and validation
242+
// attributes when no target is specified. In that case, we will merge them with the MVVM Toolkit items.
243+
// This allows us to try to keep the attributes in the same attribute list, rather than splitting them.
244+
if (toolkitTypeSymbols.ContainsValue(attributeSymbol) || (isAnnotationOrValidationAttribute && attributeListSyntax.Target is null))
245+
{
246+
mvvmToolkitAttributes.Add(attributeSyntax);
247+
}
248+
else if (isAnnotationOrValidationAttribute)
249+
{
250+
annotationOrValidationAttributes.Add(attributeSyntax);
251+
}
252+
else
253+
{
254+
fieldAttributes.Add(attributeSyntax);
255+
}
256+
}
257+
258+
// We need to start inserting the new lists right before the one we're currently
259+
// processing. We'll be removing it when we're done, the buckets will replace it.
260+
int insertionIndex = i;
261+
262+
// Helper to process and insert the new synthesized attribute lists into the target collection
263+
void InsertAttributeListIfNeeded(List<AttributeSyntax> attributes, AttributeTargetSpecifierSyntax? attributeTarget)
264+
{
265+
if (attributes is [])
266+
{
267+
return;
268+
}
269+
270+
AttributeListSyntax attributeList = AttributeList(SeparatedList(attributes)).WithTarget(attributeTarget);
271+
272+
// Only if this is the first non empty list we're adding, carry over the original trivia
273+
if (insertionIndex == i)
274+
{
275+
attributeList = attributeList.WithTriviaFrom(attributeListSyntax);
276+
}
277+
278+
// Finally, insert the new list into the final tree
279+
propertyAttributes.Insert(insertionIndex++, attributeList);
280+
}
281+
282+
InsertAttributeListIfNeeded(mvvmToolkitAttributes, attributeTarget: null);
283+
InsertAttributeListIfNeeded(annotationOrValidationAttributes, attributeTarget: attributeListSyntax.Target);
284+
InsertAttributeListIfNeeded(fieldAttributes, attributeTarget: AttributeTargetSpecifier(Token(SyntaxKind.FieldKeyword)));
285+
286+
// Remove the attribute list that we have just split into buckets
287+
propertyAttributes.RemoveAt(insertionIndex);
288+
289+
// Move the current loop iteration to the last inserted item.
290+
// We decrement by 1 because the new loop iteration will add 1.
291+
i = insertionIndex - 1;
224292
}
225293
}
226294

0 commit comments

Comments
 (0)