Skip to content

Instantly share code, notes, and snippets.

@KirillOsenkov
Created September 26, 2025 00:59
Show Gist options
  • Save KirillOsenkov/5aafbac9461e657484b2b320afaf6ac2 to your computer and use it in GitHub Desktop.
Save KirillOsenkov/5aafbac9461e657484b2b320afaf6ac2 to your computer and use it in GitHub Desktop.
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.Text;
[ExportCodeRefactoringProvider(LanguageNames.CSharp)]
public class ConvertParamsCallSitesToUseArrays : CodeRefactoringProvider
{
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var document = context.Document;
var project = document.Project;
var solution = project.Solution;
var root = await document.GetSyntaxRootAsync();
var token = root.FindToken(context.Span.Start);
if (!token.IsKind(SyntaxKind.ParamsKeyword))
{
return;
}
var parameter = token.Parent as ParameterSyntax;
if (parameter == null)
{
return;
}
var methodSyntax = parameter.FirstAncestorOrSelf<BaseMethodDeclarationSyntax>();
if (methodSyntax == null)
{
return;
}
var semanticModel = await document.GetSemanticModelAsync();
var methodSymbol = semanticModel.GetDeclaredSymbol(methodSyntax);
if (methodSymbol == null)
{
return;
}
var action = CodeAction.Create("Convert params call sites to use arrays", async ct =>
{
var callsites = await SymbolFinder.FindCallersAsync(methodSymbol, solution);
var editsByDocumentId = new Dictionary<DocumentId, List<TextChange>>();
foreach (var callsite in callsites)
{
foreach (var location in callsite.Locations)
{
var tree = location.SourceTree;
var root = await tree.GetRootAsync();
var document = solution.GetDocument(tree);
var semanticModel = await document.GetSemanticModelAsync();
var methodNameNode = root.FindNode(location.SourceSpan);
var invocationSyntax = methodNameNode.FirstAncestorOrSelf<InvocationExpressionSyntax>();
if (invocationSyntax == null)
{
continue;
}
var operation = semanticModel.GetOperation(invocationSyntax) as IInvocationOperation;
if (operation == null)
{
continue;
}
int paramsIndex = -1;
var operationArguments = operation.Arguments;
for (var i = 0; i < operationArguments.Length; i++)
{
IArgumentOperation operationArgument = operationArguments[i];
if (operationArgument.ArgumentKind is ArgumentKind.ParamArray or ArgumentKind.ParamCollection)
{
if (operationArgument.Parameter.Name == parameter.Identifier.Text)
{
paramsIndex = i;
break;
}
}
}
var args = invocationSyntax.ArgumentList.Arguments;
if (paramsIndex >= 0 && paramsIndex < args.Count)
{
if (!editsByDocumentId.TryGetValue(document.Id, out var list))
{
list = new();
editsByDocumentId[document.Id] = list;
}
var textChange = new TextChange(new TextSpan(args[paramsIndex].SpanStart, 0), "[");
list.Add(textChange);
textChange = new TextChange(new TextSpan(args[args.Count - 1].Span.End, 0), "]");
list.Add(textChange);
}
}
}
foreach (var kvp in editsByDocumentId)
{
var document = solution.GetDocument(kvp.Key);
var text = await document.GetTextAsync();
text = text.WithChanges(kvp.Value);
document = document.WithText(text);
solution = document.Project.Solution;
}
return solution;
});
context.RegisterRefactoring(action);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment