Created
September 14, 2019 02:39
-
-
Save jnm2/06f714546408ba686c1278020256bada to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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