@@ -84,10 +84,25 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
8484
8585 // Lambdas / anonymous functions do not inherit unsafe context from an enclosing
8686 // member, so we cannot fix a diagnostic that lives in one by wrapping an outer
87- // statement. Bail out if we cross such a boundary before finding any statement.
87+ // statement.
8888 var containingStatement = FindContainingStatement ( node ) ;
8989 if ( containingStatement is null )
90+ {
91+ // Expression-bodied members (e.g. 'int M() => Helper();') have no enclosing
92+ // statement. If we can rewrite the arrow body into a block body, offer that fix.
93+ var arrow = FindContainingArrowBody ( node ) ;
94+ if ( arrow is null )
95+ return ;
96+
97+ var semanticModelForArrow = await document . GetSemanticModelAsync ( context . CancellationToken ) . ConfigureAwait ( false ) ;
98+ context . RegisterCodeFix (
99+ CodeAction . Create (
100+ title : WrapStatementTitle ,
101+ createChangedDocument : ct => WrapArrowBodyAsync ( document , arrow , semanticModelForArrow , ct ) ,
102+ equivalenceKey : WrapStatementTitle ) ,
103+ diagnostic ) ;
90104 return ;
105+ }
91106
92107 // Skip statements whose tokens enclose a preprocessor directive (e.g. an argument
93108 // list with #if/#else/#endif between commas). Wrapping such a statement would
@@ -149,6 +164,24 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context)
149164 return null ;
150165 }
151166
167+ /// <summary>
168+ /// Finds the smallest enclosing <see cref="ArrowExpressionClauseSyntax"/> (the body
169+ /// of an expression-bodied member or accessor), stopping at lambda / anonymous
170+ /// function boundaries: an expression-bodied lambda is not a member body we can
171+ /// rewrite.
172+ /// </summary>
173+ private static ArrowExpressionClauseSyntax ? FindContainingArrowBody ( SyntaxNode node )
174+ {
175+ for ( var n = node ; n is not null ; n = n . Parent )
176+ {
177+ if ( n is ArrowExpressionClauseSyntax arrow )
178+ return arrow ;
179+ if ( n is AnonymousFunctionExpressionSyntax )
180+ return null ;
181+ }
182+ return null ;
183+ }
184+
152185 /// <summary>
153186 /// Finds the smallest method-like declaration that contains the diagnostic, stopping
154187 /// at lambda boundaries (unsafe context does not flow into lambdas, so wrapping the
@@ -356,6 +389,107 @@ private static async Task<Document> WrapMemberBodyAsync(Document document, Block
356389 return document . WithSyntaxRoot ( root . ReplaceNode ( body , newBody ) ) ;
357390 }
358391
392+ // ---- Wrapping an expression-bodied member ----
393+
394+ private static async Task < Document > WrapArrowBodyAsync (
395+ Document document , ArrowExpressionClauseSyntax arrow , SemanticModel ? semanticModel , CancellationToken ct )
396+ {
397+ var root = await document . GetSyntaxRootAsync ( ct ) . ConfigureAwait ( false ) ;
398+ if ( root is null || arrow . Parent is not { } member )
399+ return document ;
400+
401+ bool requiresReturn = ArrowBodyRequiresReturn ( member , semanticModel ) ;
402+
403+ // Build 'return <expr>;' or '<expr>;' depending on the member's effective return type.
404+ // Preserve the original expression's trivia inside the new statement so any inline
405+ // comments authored on the arrow expression survive.
406+ StatementSyntax inner = requiresReturn
407+ ? SyntaxFactory . ReturnStatement ( arrow . Expression . WithoutTrivia ( ) )
408+ : SyntaxFactory . ExpressionStatement ( arrow . Expression . WithoutTrivia ( ) ) ;
409+ var unsafeBlock = BuildUnsafeBlock ( [ inner ] ) ;
410+ var newBody = SyntaxFactory . Block ( unsafeBlock ) . WithAdditionalAnnotations ( Formatter . Annotation ) ;
411+
412+ // Replace the member's '=> expr;' with '{ unsafe { ... } }'.
413+ var newMember = ReplaceArrowWithBlock ( member , newBody ) ;
414+ if ( newMember is null )
415+ return document ;
416+
417+ return document . WithSyntaxRoot ( root . ReplaceNode ( member , newMember ) ) ;
418+ }
419+
420+ /// <summary>
421+ /// True if rewriting <paramref name="memberWithArrowBody"/>'s arrow body into a block
422+ /// body requires a <c>return</c> statement around the original expression. False when
423+ /// the member produces no value (void method/local-function, set/init/add/remove
424+ /// accessor, constructor/destructor, or an async method whose only result is the Task).
425+ /// </summary>
426+ private static bool ArrowBodyRequiresReturn ( SyntaxNode memberWithArrowBody , SemanticModel ? semanticModel )
427+ {
428+ switch ( memberWithArrowBody )
429+ {
430+ case MethodDeclarationSyntax m :
431+ return ! IsVoidLikeMethod ( m . ReturnType , m . Modifiers , semanticModel , m ) ;
432+ case LocalFunctionStatementSyntax lf :
433+ return ! IsVoidLikeMethod ( lf . ReturnType , lf . Modifiers , semanticModel , lf ) ;
434+ case OperatorDeclarationSyntax :
435+ case ConversionOperatorDeclarationSyntax :
436+ case PropertyDeclarationSyntax :
437+ case IndexerDeclarationSyntax :
438+ return true ;
439+ case AccessorDeclarationSyntax a :
440+ return a . Keyword . IsKind ( SyntaxKind . GetKeyword ) ;
441+ case ConstructorDeclarationSyntax :
442+ case DestructorDeclarationSyntax :
443+ return false ;
444+ default :
445+ // Unknown member kind - rewriting could produce broken syntax; assume no return.
446+ return false ;
447+ }
448+ }
449+
450+ private static bool IsVoidLikeMethod ( TypeSyntax returnType , SyntaxTokenList modifiers , SemanticModel ? semanticModel , SyntaxNode declaration )
451+ {
452+ if ( returnType is PredefinedTypeSyntax p && p . Keyword . IsKind ( SyntaxKind . VoidKeyword ) )
453+ return true ;
454+
455+ // 'async Task' / 'async ValueTask' bodies do not return a value; the await/return
456+ // expression is just an expression statement when in block form.
457+ if ( modifiers . Any ( SyntaxKind . AsyncKeyword ) && semanticModel is not null
458+ && semanticModel . GetDeclaredSymbol ( declaration ) is IMethodSymbol m
459+ && m . ReturnType is INamedTypeSymbol rt
460+ && rt . TypeArguments . Length == 0
461+ && rt . Name is "Task" or "ValueTask" )
462+ {
463+ return true ;
464+ }
465+
466+ return false ;
467+ }
468+
469+ private static SyntaxNode ? ReplaceArrowWithBlock ( SyntaxNode member , BlockSyntax newBody ) => member switch
470+ {
471+ MethodDeclarationSyntax m => m . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
472+ LocalFunctionStatementSyntax lf => lf . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
473+ OperatorDeclarationSyntax op => op . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
474+ ConversionOperatorDeclarationSyntax co => co . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
475+ ConstructorDeclarationSyntax c => c . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
476+ DestructorDeclarationSyntax d => d . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
477+ AccessorDeclarationSyntax a => a . WithExpressionBody ( null ) . WithBody ( newBody ) . WithSemicolonToken ( default ) ,
478+ // For arrow-bodied property/indexer ('int P => expr;'), turn it into a block with a
479+ // single get accessor whose body is the new unsafe block.
480+ PropertyDeclarationSyntax prop => prop
481+ . WithExpressionBody ( null )
482+ . WithSemicolonToken ( default )
483+ . WithAccessorList ( SyntaxFactory . AccessorList ( SyntaxFactory . SingletonList (
484+ SyntaxFactory . AccessorDeclaration ( SyntaxKind . GetAccessorDeclaration ) . WithBody ( newBody ) ) ) ) ,
485+ IndexerDeclarationSyntax idx => idx
486+ . WithExpressionBody ( null )
487+ . WithSemicolonToken ( default )
488+ . WithAccessorList ( SyntaxFactory . AccessorList ( SyntaxFactory . SingletonList (
489+ SyntaxFactory . AccessorDeclaration ( SyntaxKind . GetAccessorDeclaration ) . WithBody ( newBody ) ) ) ) ,
490+ _ => null ,
491+ } ;
492+
359493 // ---- Wrapping a single statement ----
360494
361495 private static async Task < Document > WrapSingleStatementAsync (
@@ -378,8 +512,12 @@ private static async Task<Document> WrapSingleStatementAsync(
378512 var rewrite = TryRewriteAsForwardDeclaration ( ( LocalDeclarationStatementSyntax ) statement , semanticModel ) ;
379513 if ( rewrite is null )
380514 {
381- // Should not happen - ChooseFixStrategy already verified the rewrite is possible.
382- // Fall back to wrap-as-is to avoid producing an empty fix.
515+ // Defensive: ChooseFixStrategy already verified the rewrite is possible by
516+ // calling TryRewriteAsForwardDeclaration, so this branch should be unreachable.
517+ // If it is reached (e.g. a future semantic-model change makes the rewrite fail
518+ // here but not at strategy-decision time), bail out unchanged rather than
519+ // producing a broken fix; wrap-as-is would not be safe because the local is
520+ // referenced after the wrap point (that is why we picked ForwardDeclare).
383521 return document ;
384522 }
385523
0 commit comments