@@ -214,6 +214,31 @@ when string.Equals(ctParam.Name, "ct", StringComparison.Ordinal):
214214
215215 case AsyncMethodConventionsAnalyzer . CancellationTokenNameId :
216216 {
217+ var isInterfaceMethod = methodSymbol . ContainingType ? . TypeKind == TypeKind . Interface ;
218+
219+ if ( isInterfaceMethod )
220+ {
221+ const string title = "Rename CancellationToken parameter to 'ct' (interface and implementations)" ;
222+
223+ context . RegisterCodeFix (
224+ CodeAction . Create (
225+ title ,
226+ c => RenameCancellationTokenForInterfaceAndImplementationsAsync (
227+ context . Document . Project . Solution ,
228+ methodSymbol ,
229+ c ) ,
230+ "RenameCt_InterfaceAndImpls" ) ,
231+ diagnostic ) ;
232+
233+ return ;
234+ }
235+
236+ // Non-interface method: simple symbol rename
237+ if ( string . Equals ( ctParam . Name , "ct" , StringComparison . Ordinal ) )
238+ {
239+ return ;
240+ }
241+
217242 var renameTitle = $ "Rename '{ ctParam . Name } ' to 'ct'";
218243
219244 context . RegisterCodeFix (
@@ -959,4 +984,129 @@ private static async Task<Document> MoveCancellationTokenToLastInLambdaAsync(Doc
959984 var newRoot = root . ReplaceNode ( parenthesized , newLambda ) ;
960985 return document . WithSyntaxRoot ( newRoot ) ;
961986 }
987+
988+ private static async Task < Solution > RenameCancellationTokenForInterfaceAndImplementationsAsync ( Solution solution ,
989+ IMethodSymbol interfaceMethod ,
990+ CancellationToken cancellationToken )
991+ {
992+ // 1. Find CT parameter ordinal on the interface
993+ var ctIndex = - 1 ;
994+ for ( var i = 0 ; i < interfaceMethod . Parameters . Length ; i ++ )
995+ {
996+ if ( IsCancellationToken ( interfaceMethod . Parameters [ i ] . Type ) )
997+ {
998+ ctIndex = i ;
999+ break ;
1000+ }
1001+ }
1002+
1003+ if ( ctIndex < 0 )
1004+ {
1005+ // Interface no longer has CT? Nothing to do.
1006+ return solution ;
1007+ }
1008+
1009+ // 2. Collect interface + implementing methods
1010+ var allMethods = ImmutableArray . CreateBuilder < IMethodSymbol > ( ) ;
1011+ allMethods . Add ( interfaceMethod ) ;
1012+
1013+ var impls = await SymbolFinder . FindImplementationsAsync (
1014+ interfaceMethod ,
1015+ solution ,
1016+ projects : null ,
1017+ cancellationToken )
1018+ . ConfigureAwait ( false ) ;
1019+
1020+ foreach ( var impl in impls . OfType < IMethodSymbol > ( ) )
1021+ {
1022+ if ( impl . DeclaringSyntaxReferences . Length > 0 )
1023+ {
1024+ allMethods . Add ( impl ) ;
1025+ }
1026+ }
1027+
1028+ // 3. Group by document
1029+ var methodsByDocument = new Dictionary < DocumentId , ImmutableArray < MethodDeclarationSyntax > . Builder > ( ) ;
1030+
1031+ foreach ( var method in allMethods )
1032+ {
1033+ foreach ( var syntaxRef in method . DeclaringSyntaxReferences )
1034+ {
1035+ var syntax = await syntaxRef . GetSyntaxAsync ( cancellationToken )
1036+ . ConfigureAwait ( false ) ;
1037+ if ( syntax is not MethodDeclarationSyntax methodDecl )
1038+ {
1039+ continue ;
1040+ }
1041+
1042+ var doc = solution . GetDocument ( methodDecl . SyntaxTree ) ;
1043+ if ( doc is null )
1044+ {
1045+ continue ;
1046+ }
1047+
1048+ var docId = doc . Id ;
1049+ if ( ! methodsByDocument . TryGetValue ( docId , out var list ) )
1050+ {
1051+ list = ImmutableArray . CreateBuilder < MethodDeclarationSyntax > ( ) ;
1052+ methodsByDocument [ docId ] = list ;
1053+ }
1054+
1055+ list . Add ( methodDecl ) ;
1056+ }
1057+ }
1058+
1059+ // 4. Rewrite name in each method declaration at the same ordinal
1060+ foreach ( var kvp in methodsByDocument )
1061+ {
1062+ var docId = kvp . Key ;
1063+ var methodDecls = kvp . Value . ToImmutable ( ) ;
1064+
1065+ var document = solution . GetDocument ( docId ) ;
1066+ if ( document is null )
1067+ {
1068+ continue ;
1069+ }
1070+
1071+ var root = await document . GetSyntaxRootAsync ( cancellationToken )
1072+ . ConfigureAwait ( false ) ;
1073+ if ( root is null )
1074+ {
1075+ continue ;
1076+ }
1077+
1078+ var newRoot = root . ReplaceNodes (
1079+ methodDecls ,
1080+ ( original , _ ) =>
1081+ {
1082+ var parameters = original . ParameterList . Parameters ;
1083+ if ( ctIndex < 0 || ctIndex >= parameters . Count )
1084+ {
1085+ // Signature drifted; be conservative.
1086+ return original ;
1087+ }
1088+
1089+ var ctParamSyntax = parameters [ ctIndex ] ;
1090+
1091+ // If it's already 'ct', no change needed.
1092+ if ( ctParamSyntax . Identifier . Text == "ct" )
1093+ {
1094+ return original ;
1095+ }
1096+
1097+ var newCtParamSyntax = ctParamSyntax . WithIdentifier (
1098+ SyntaxFactory . Identifier ( "ct" )
1099+ . WithTriviaFrom ( ctParamSyntax . Identifier ) ) ;
1100+
1101+ var newParameters = parameters . Replace ( ctParamSyntax , newCtParamSyntax ) ;
1102+
1103+ return original . WithParameterList (
1104+ original . ParameterList . WithParameters ( newParameters ) ) ;
1105+ } ) ;
1106+
1107+ solution = solution . WithDocumentSyntaxRoot ( docId , newRoot ) ;
1108+ }
1109+
1110+ return solution ;
1111+ }
9621112}
0 commit comments