Skip to content

Instantly share code, notes, and snippets.

@jnm2
Created September 14, 2019 02:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jnm2/06f714546408ba686c1278020256bada to your computer and use it in GitHub Desktop.
Save jnm2/06f714546408ba686c1278020256bada to your computer and use it in GitHub Desktop.
using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public static class Program
{
public static async Task Main()
{
MSBuildLocator.RegisterDefaults();
const string solutionPath = @"C:\hardcoded\path.sln";
using (var workspace = MSBuildWorkspace.Create())
{
Console.WriteLine("Loading solution...");
var solution = await workspace.OpenSolutionAsync(solutionPath);
Console.WriteLine("Applying pascal casing to all tuple elements...");
solution = await ApplyPascalCaseToAllTupleElementNamesAsync(solution);
Console.WriteLine("Saving all changes...");
if (!workspace.TryApplyChanges(solution))
throw new InvalidOperationException("The solution should not have been modified in the meantime.");
}
Console.WriteLine("Done.");
}
private static async Task<Solution> ApplyPascalCaseToAllTupleElementNamesAsync(Solution solution)
{
foreach (var project in solution.Projects)
{
foreach (var document in project.Documents)
{
var root = await document.GetSyntaxRootAsync();
var accesses = new List<(
ExpressionSyntax Expression,
Func<ITypeSymbol, bool> IsMemberOnTupleType,
SyntaxToken Identifier)>();
foreach (var node in root.DescendantNodes())
{
switch (node)
{
case MemberAccessExpressionSyntax memberAccess:
if (memberAccess.Expression.Kind() != SyntaxKind.ThisExpression) // Optimization to avoid semantic model
{
accesses.Add((
memberAccess.Expression,
type => type.IsTupleType,
memberAccess.Name.Identifier));
}
break;
case MemberBindingExpressionSyntax memberBinding:
var conditionalAccess = memberBinding.FirstAncestorOrSelf<ConditionalAccessExpressionSyntax>();
if (conditionalAccess is { })
{
accesses.Add((
conditionalAccess.Expression,
type => type is INamedTypeSymbol
{
OriginalDefinition: { SpecialType: SpecialType.System_Nullable_T },
TypeArguments: var typeArgs
} && typeArgs[0].IsTupleType,
memberBinding.Name.Identifier));
}
break;
}
}
accesses.RemoveAll(a => !StartsWithLowerCaseChar(a.Identifier));
if (accesses.Any())
{
var semanticModel = await document.GetSemanticModelAsync();
root = root.ReplaceTokens(
accesses
.Where(a => a.IsMemberOnTupleType(semanticModel.GetTypeInfo(a.Expression).Type))
.Select(a => a.Identifier),
(original, rewritten) => ApplyPascalCasing(rewritten));
}
root = new PascalCaseTupleElementNamesRewriter().Visit(root);
solution = solution.WithDocumentSyntaxRoot(document.Id, root);
}
}
return solution;
}
private sealed class PascalCaseTupleElementNamesRewriter : CSharpSyntaxRewriter
{
public override SyntaxNode VisitTupleElement(TupleElementSyntax node)
{
return base.VisitTupleElement(node.WithIdentifier(ApplyPascalCasing(node.Identifier)));
}
public override SyntaxNode VisitTupleExpression(TupleExpressionSyntax node)
{
for (var i = 0; i < node.Arguments.Count; i++)
{
// Can't use foreach or else ReplaceToken will fail to find the token from the wrong version of the
// parent.
var argument = node.Arguments[i];
if (argument.NameColon is { Name: { Identifier: var elementNameToken } })
{
if (ApplyPascalCasingIfNeeded(elementNameToken, out var renamedToken))
{
node = GetInferredTupleElementName(argument.Expression)?.ValueText == renamedToken.ValueText
? node.RemoveNode(argument.NameColon, SyntaxRemoveOptions.KeepExteriorTrivia)
: node.ReplaceToken(elementNameToken, renamedToken);
}
}/*
else
{
if (GetInferredTupleElementName(argument.Expression) is { } inferredName
&& ApplyPascalCasingIfNeeded(inferredName, out var renamedToken))
{
node = node.ReplaceNode(argument, argument
.WithNameColon(SyntaxFactory.NameColon(
SyntaxFactory.IdentifierName(renamedToken)
.WithLeadingTrivia(argument.Expression.GetLeadingTrivia()),
SyntaxFactory.Token(
leading: default,
SyntaxKind.ColonToken,
trailing: SyntaxFactory.TriviaList(SyntaxFactory.Space))))
.WithExpression(argument.Expression.WithoutLeadingTrivia()));
}
}*/
}
return base.VisitTupleExpression(node);
}
private static SyntaxToken? GetInferredTupleElementName(ExpressionSyntax tupleElement)
{
return tupleElement switch
{
MemberAccessExpressionSyntax memberAccess => memberAccess.Name?.Identifier,
MemberBindingExpressionSyntax memberBinding => memberBinding.Name.Identifier,
ConditionalAccessExpressionSyntax conditionalAccess => GetInferredTupleElementName(conditionalAccess.WhenNotNull),
IdentifierNameSyntax identifierName => identifierName.Identifier,
_ => null
};
}
}
private static SyntaxToken ApplyPascalCasing(SyntaxToken identifierToken)
{
return ApplyPascalCasingIfNeeded(identifierToken, out var pascalCasedIdentifierToken)
? pascalCasedIdentifierToken
: identifierToken;
}
private static bool StartsWithLowerCaseChar(SyntaxToken identifierToken)
{
return identifierToken.ValueText is { } name && char.IsLower(name[0]);
}
private static bool ApplyPascalCasingIfNeeded(SyntaxToken identifierToken, out SyntaxToken renamedToken)
{
if (StartsWithLowerCaseChar(identifierToken))
{
var pascalCasedName = char.ToUpperInvariant(identifierToken.ValueText[0]) + identifierToken.ValueText.Substring(1);
renamedToken = SyntaxFactory.Identifier(pascalCasedName).WithTriviaFrom(identifierToken);
return true;
}
renamedToken = default;
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment