// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; using System.Diagnostics; using System.Linq.Expressions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Microsoft.AspNetCore.Mvc.Razor.Internal { /// /// An expression rewriter which can hoist a simple expression lambda into a private field. /// public class ExpressionRewriter : CSharpSyntaxRewriter { private static readonly string FieldNameTemplate = "__h{0}"; public ExpressionRewriter(SemanticModel semanticModel) { SemanticModel = semanticModel; Expressions = new List>(); } // We only want to rewrite expressions for the top-level class definition. private bool IsInsideClass { get; set; } private SemanticModel SemanticModel { get; } private List> Expressions { get; } public static CSharpCompilation Rewrite(CSharpCompilation compilation) { var rewrittenTrees = new List(); foreach (var tree in compilation.SyntaxTrees) { var semanticModel = compilation.GetSemanticModel(tree, ignoreAccessibility: true); var rewriter = new ExpressionRewriter(semanticModel); var rewrittenTree = tree.WithRootAndOptions(rewriter.Visit(tree.GetRoot()), tree.Options); rewrittenTrees.Add(rewrittenTree); } return compilation.RemoveAllSyntaxTrees().AddSyntaxTrees(rewrittenTrees); } public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node) { if (IsInsideClass) { // Avoid recursing into nested classes. return node; } Expressions.Clear(); IsInsideClass = true; // Call base first to visit all the children and populate Expressions. var classDeclaration = (ClassDeclarationSyntax)base.VisitClassDeclaration(node); IsInsideClass = false; var memberDeclarations = new List(); foreach (var kvp in Expressions) { var expression = kvp.Key; var memberName = kvp.Value.GetFirstToken(); var expressionType = SemanticModel.GetTypeInfo(expression).ConvertedType; var declaration = SyntaxFactory.FieldDeclaration( SyntaxFactory.List(), SyntaxFactory.TokenList( SyntaxFactory.Token(SyntaxKind.PrivateKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)), SyntaxFactory.VariableDeclaration( SyntaxFactory.ParseTypeName(expressionType.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat)), SyntaxFactory.SingletonSeparatedList( SyntaxFactory.VariableDeclarator( memberName, SyntaxFactory.BracketedArgumentList(), SyntaxFactory.EqualsValueClause(expression))))) .WithTriviaFrom(expression); memberDeclarations.Add(declaration); } return classDeclaration.AddMembers(memberDeclarations.ToArray()); } public override SyntaxNode VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node) { Debug.Assert(IsInsideClass); // If this lambda is an Expression and is suitable for hoisting, we rewrite this into a field access. // // Before: // public Task ExecuteAsync(...) // { // ... // Html.EditorFor(m => m.Price); // ... // } // // // After: // private static readonly Expression> __h0 = m => m.Price; // public Task ExecuteAsync(...) // { // ... // Html.EditorFor(__h0); // ... // } // var type = SemanticModel.GetTypeInfo(node); // Due to an anomaly where Roslyn (depending on code sample) may finish compilation without diagnostic // errors (this code path does not execute when diagnostic errors are present) we need to validate that // the ConvertedType was determined/is not null. if (type.ConvertedType == null || type.ConvertedType.Name != typeof(Expression).Name && type.ConvertedType.ContainingNamespace.Name != typeof(Expression).Namespace) { return node; } if (!node.Parent.IsKind(SyntaxKind.Argument)) { return node; } var parameter = node.Parameter; if (IsValidForHoisting(parameter, node.Body)) { // Replace with a MemberAccess var memberName = string.Format(FieldNameTemplate, Expressions.Count); var memberAccess = PadMemberAccess(node, SyntaxFactory.IdentifierName(memberName)); Expressions.Add(new KeyValuePair(node, memberAccess)); return memberAccess; } return node; } private static IdentifierNameSyntax PadMemberAccess( SimpleLambdaExpressionSyntax node, IdentifierNameSyntax memberAccess) { var charactersToExclude = memberAccess.Identifier.Text.Length; var triviaList = new SyntaxTriviaList(); // Go through each token and // 1. Append leading trivia // 2. Append the same number of whitespace as the length of the token text // 3. Append trailing trivia foreach (var token in node.DescendantTokens()) { if (token.HasLeadingTrivia) { triviaList = triviaList.AddRange(token.LeadingTrivia); } // Need to exclude the length of the member name from the padding. var padding = token.Text.Length; if (padding > charactersToExclude) { padding -= charactersToExclude; charactersToExclude = 0; } else { charactersToExclude -= padding; padding = 0; } if (padding > 0) { triviaList = triviaList.Add(SyntaxFactory.Whitespace(new string(' ', padding))); } if (token.HasTrailingTrivia) { triviaList = triviaList.AddRange(token.TrailingTrivia); } } return memberAccess .WithLeadingTrivia(node.GetLeadingTrivia()) .WithTrailingTrivia(triviaList); } private static bool IsValidForHoisting(ParameterSyntax parameter, CSharpSyntaxNode node) { if (node.IsKind(SyntaxKind.IdentifierName)) { var identifier = (IdentifierNameSyntax)node; if (identifier.Identifier.Text == parameter.Identifier.Text) { return true; } } else if (node.IsKind(SyntaxKind.SimpleMemberAccessExpression)) { var memberAccess = (MemberAccessExpressionSyntax)node; var lhs = memberAccess.Expression; return IsValidForHoisting(parameter, lhs); } return false; } } }