88using Microsoft . CodeAnalysis . CodeFixes ;
99using Microsoft . CodeAnalysis . CSharp ;
1010using Microsoft . CodeAnalysis . CSharp . Syntax ;
11+ using Microsoft . CodeAnalysis . Editing ;
1112
1213namespace EntityFrameworkCore . Triggered . Analyzers . CodeFixes ;
1314
@@ -22,120 +23,103 @@ public sealed class MigrateToAsyncTriggerCodeFixProvider : CodeFixProvider
2223 public override async Task RegisterCodeFixesAsync ( CodeFixContext context )
2324 {
2425 var diagnostic = context . Diagnostics . First ( ) ;
25- var properties = diagnostic . Properties ;
26+ var document = context . Document ;
2627
27- if ( ! properties . TryGetValue ( "AsyncInterfaceShortName" , out var asyncInterfaceName ) ||
28- ! properties . TryGetValue ( "SyncInterfaceShortName" , out var syncInterfaceName ) ||
29- ! properties . TryGetValue ( "SyncMethodName" , out var syncMethodName ) ||
30- ! properties . TryGetValue ( "AsyncMethodName" , out var asyncMethodName ) )
28+ var root = await document . GetSyntaxRootAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
29+ var semanticModel = await document . GetSemanticModelAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
30+ if ( root is null || semanticModel is null )
3131 return ;
3232
33- var root = await context . Document . GetSyntaxRootAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
34- if ( root == null ) return ;
33+ var methodDeclaration = root . FindNode ( diagnostic . Location . SourceSpan ) . FirstAncestorOrSelf < MethodDeclarationSyntax > ( ) ;
34+ if ( methodDeclaration is null || ! TriggerMigrationMatcher . TryMatch ( methodDeclaration , semanticModel , context . CancellationToken , out var match ) )
35+ return ;
36+
37+ if ( methodDeclaration . ExplicitInterfaceSpecifier is { } specifier && GetRightmostIdentifier ( specifier . Name ) != match . Entry . SyncInterfaceShortName )
38+ return ;
3539
36- var node = root . FindNode ( diagnostic . Location . SourceSpan ) ;
37- var methodDeclaration = node . FirstAncestorOrSelf < MethodDeclarationSyntax > ( ) ;
38- if ( methodDeclaration == null ) return ;
40+ var baseType = await FindDeclaringBaseTypeAsync ( document . Project . Solution , match , context . CancellationToken ) . ConfigureAwait ( false ) ;
3941
40- var classDeclaration = methodDeclaration . FirstAncestorOrSelf < ClassDeclarationSyntax > ( ) ;
41- if ( classDeclaration == null ) return ;
42+ // The interface is inherited from a base class or written through an alias; renaming it here is not safe
43+ if ( baseType is null )
44+ return ;
4245
4346 context . RegisterCodeFix (
4447 CodeAction . Create (
45- title : $ "Migrate to { asyncInterfaceName } ",
46- createChangedDocument : ct => MigrateToAsync ( context . Document , root , classDeclaration , methodDeclaration , syncInterfaceName ! , asyncInterfaceName ! , syncMethodName ! , asyncMethodName ! , ct ) ,
48+ title : $ "Migrate to { match . Entry . AsyncInterfaceShortName } ",
49+ createChangedSolution : ct => MigrateToAsyncAsync ( document , methodDeclaration , baseType . Value . Document , baseType . Value . Node , match . Entry , ct ) ,
4750 equivalenceKey : "MigrateToAsyncTrigger" ) ,
4851 diagnostic ) ;
4952 }
5053
51- private static Task < Document > MigrateToAsync (
52- Document document ,
53- SyntaxNode root ,
54- ClassDeclarationSyntax classDeclaration ,
55- MethodDeclarationSyntax methodDeclaration ,
56- string syncInterfaceName ,
57- string asyncInterfaceName ,
58- string syncMethodName ,
59- string asyncMethodName ,
60- CancellationToken cancellationToken )
54+ private static async Task < ( Document Document , BaseTypeSyntax Node ) ? > FindDeclaringBaseTypeAsync ( Solution solution , TriggerMigrationMatch match , CancellationToken cancellationToken )
6155 {
62- var newRoot = root ;
63-
64- // Replace interface name in base list
65- if ( classDeclaration . BaseList != null )
56+ foreach ( var syntaxReference in match . Method . ContainingType . DeclaringSyntaxReferences )
6657 {
67- var newBaseList = classDeclaration . BaseList ;
68- foreach ( var baseType in classDeclaration . BaseList . Types )
58+ if ( await syntaxReference . GetSyntaxAsync ( cancellationToken ) . ConfigureAwait ( false ) is not TypeDeclarationSyntax { BaseList : { } baseList } )
59+ continue ;
60+
61+ var document = solution . GetDocument ( baseList . SyntaxTree ) ;
62+ var semanticModel = document is null ? null : await document . GetSemanticModelAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
63+ if ( semanticModel is null )
64+ continue ;
65+
66+ foreach ( var baseType in baseList . Types )
6967 {
70- var typeName = GetBaseTypeName ( baseType ) ;
71- if ( typeName == syncInterfaceName )
68+ if ( SymbolEqualityComparer . Default . Equals ( semanticModel . GetTypeInfo ( baseType . Type , cancellationToken ) . Type , match . TriggerInterface )
69+ && GetRightmostIdentifier ( baseType . Type ) == match . Entry . SyncInterfaceShortName )
7270 {
73- var newBaseType = ReplaceInterfaceName ( baseType , syncInterfaceName , asyncInterfaceName ) ;
74- newBaseList = newBaseList . ReplaceNode ( baseType , newBaseType ) ;
71+ return ( document ! , baseType ) ;
7572 }
7673 }
77-
78- if ( newBaseList != classDeclaration . BaseList )
79- {
80- newRoot = newRoot . ReplaceNode ( classDeclaration . BaseList , newBaseList ) ;
81- }
8274 }
8375
84- // Find the method again in the updated tree
85- var updatedMethod = newRoot . FindNode ( methodDeclaration . Identifier . Span )
86- . FirstAncestorOrSelf < MethodDeclarationSyntax > ( ) ;
76+ return null ;
77+ }
78+
79+ private static async Task < Solution > MigrateToAsyncAsync (
80+ Document methodDocument ,
81+ MethodDeclarationSyntax methodDeclaration ,
82+ Document baseTypeDocument ,
83+ BaseTypeSyntax baseType ,
84+ TriggerMappingEntry entry ,
85+ CancellationToken cancellationToken )
86+ {
87+ var editor = new SolutionEditor ( methodDocument . Project . Solution ) ;
88+
89+ var baseTypeEditor = await editor . GetDocumentEditorAsync ( baseTypeDocument . Id , cancellationToken ) . ConfigureAwait ( false ) ;
90+ baseTypeEditor . ReplaceNode ( baseType . Type , RenameRightmostIdentifier ( baseType . Type , entry . AsyncInterfaceShortName ) ) ;
91+
92+ var newMethod = methodDeclaration . WithIdentifier (
93+ SyntaxFactory . Identifier ( entry . AsyncMethodName ) . WithTriviaFrom ( methodDeclaration . Identifier ) ) ;
8794
88- if ( updatedMethod != null && updatedMethod . Identifier . Text == syncMethodName )
95+ if ( newMethod . ExplicitInterfaceSpecifier is { } specifier )
8996 {
90- var newMethod = updatedMethod . WithIdentifier (
91- SyntaxFactory . Identifier ( asyncMethodName )
92- . WithTriviaFrom ( updatedMethod . Identifier ) ) ;
93- newRoot = newRoot . ReplaceNode ( updatedMethod , newMethod ) ;
97+ newMethod = newMethod . WithExplicitInterfaceSpecifier (
98+ specifier . WithName ( ( NameSyntax ) RenameRightmostIdentifier ( specifier . Name , entry . AsyncInterfaceShortName ) ) ) ;
9499 }
95100
96- return Task . FromResult ( document . WithSyntaxRoot ( newRoot ) ) ;
97- }
101+ var methodEditor = await editor . GetDocumentEditorAsync ( methodDocument . Id , cancellationToken ) . ConfigureAwait ( false ) ;
102+ methodEditor . ReplaceNode ( methodDeclaration , newMethod ) ;
98103
99- private static string ? GetBaseTypeName ( BaseTypeSyntax baseType )
100- {
101- return baseType . Type switch
102- {
103- SimpleNameSyntax simple => simple . Identifier . Text ,
104- QualifiedNameSyntax qualified => qualified . Right . Identifier . Text ,
105- _ => null
106- } ;
104+ return editor . GetChangedSolution ( ) ;
107105 }
108106
109- private static BaseTypeSyntax ReplaceInterfaceName ( BaseTypeSyntax baseType , string oldName , string newName )
107+ private static string ? GetRightmostIdentifier ( TypeSyntax type ) => type switch
110108 {
111- switch ( baseType . Type )
112- {
113- case GenericNameSyntax generic when generic . Identifier . Text == oldName :
114- var newGeneric = generic . WithIdentifier (
115- SyntaxFactory . Identifier ( newName ) . WithTriviaFrom ( generic . Identifier ) ) ;
116- return baseType . WithType ( newGeneric ) ;
117-
118- case SimpleNameSyntax simple when simple . Identifier . Text == oldName :
119- var newSimple = SyntaxFactory . IdentifierName (
120- SyntaxFactory . Identifier ( newName ) . WithTriviaFrom ( simple . Identifier ) ) ;
121- return baseType . WithType ( newSimple ) ;
122-
123- case QualifiedNameSyntax qualified when qualified . Right . Identifier . Text == oldName :
124- SimpleNameSyntax newRight ;
125- if ( qualified . Right is GenericNameSyntax rightGeneric )
126- {
127- newRight = rightGeneric . WithIdentifier (
128- SyntaxFactory . Identifier ( newName ) . WithTriviaFrom ( rightGeneric . Identifier ) ) ;
129- }
130- else
131- {
132- newRight = SyntaxFactory . IdentifierName (
133- SyntaxFactory . Identifier ( newName ) . WithTriviaFrom ( qualified . Right . Identifier ) ) ;
134- }
135- return baseType . WithType ( qualified . WithRight ( newRight ) ) ;
109+ SimpleNameSyntax simple => simple . Identifier . ValueText ,
110+ QualifiedNameSyntax qualified => qualified . Right . Identifier . ValueText ,
111+ AliasQualifiedNameSyntax aliasQualified => aliasQualified . Name . Identifier . ValueText ,
112+ _ => null
113+ } ;
136114
137- default :
138- return baseType ;
139- }
140- }
115+ private static TypeSyntax RenameRightmostIdentifier ( TypeSyntax type , string newName ) => type switch
116+ {
117+ SimpleNameSyntax simple => RenameSimpleName ( simple , newName ) ,
118+ QualifiedNameSyntax qualified => qualified . WithRight ( RenameSimpleName ( qualified . Right , newName ) ) ,
119+ AliasQualifiedNameSyntax aliasQualified => aliasQualified . WithName ( RenameSimpleName ( aliasQualified . Name , newName ) ) ,
120+ _ => type
121+ } ;
122+
123+ private static SimpleNameSyntax RenameSimpleName ( SimpleNameSyntax name , string newName )
124+ => name . WithIdentifier ( SyntaxFactory . Identifier ( newName ) . WithTriviaFrom ( name . Identifier ) ) ;
141125}
0 commit comments