@@ -100,13 +100,6 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
100
100
if ( root ! . FindNode ( diagnosticSpan ) . FirstAncestorOrSelf < FieldDeclarationSyntax > ( ) is { Declaration . Variables : [ { Identifier . Text : string identifierName } ] } fieldDeclaration &&
101
101
identifierName == fieldName )
102
102
{
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
-
110
103
// Register the code fix to update the class declaration to inherit from ObservableObject instead
111
104
context . RegisterCodeFix (
112
105
CodeAction . Create (
@@ -194,33 +187,108 @@ private static async Task<Document> ConvertToPartialProperty(
194
187
continue ;
195
188
}
196
189
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 )
200
191
{
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
+ }
210
219
}
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
220
221
{
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 ;
224
292
}
225
293
}
226
294
0 commit comments