|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.Simplification.Simplifiers;
internal sealed partial class ExpressionSimplifier : AbstractCSharpSimplifier<ExpressionSyntax, ExpressionSyntax>
{
public static readonly ExpressionSimplifier Instance = new();
private ExpressionSimplifier()
{
}
public override bool TrySimplify(
ExpressionSyntax expression,
SemanticModel semanticModel,
CSharpSimplifierOptions options,
[NotNullWhen(true)] out ExpressionSyntax? replacementNode,
out TextSpan issueSpan,
CancellationToken cancellationToken)
{
replacementNode = null;
issueSpan = default;
if (expression is MemberAccessExpressionSyntax { Expression: ThisExpressionSyntax } memberAccessExpression)
{
if (!MemberAccessExpressionSimplifier.Instance.ShouldSimplifyThisMemberAccessExpression(
memberAccessExpression, semanticModel, options, out _, out _, cancellationToken))
{
return false;
}
replacementNode = memberAccessExpression.GetNameWithTriviaMoved();
issueSpan = memberAccessExpression.Expression.Span;
return true;
}
if (TryReduceExplicitName(expression, semanticModel, out var replacementTypeNode, out issueSpan, options, cancellationToken))
{
replacementNode = replacementTypeNode;
return true;
}
return TrySimplify(expression, semanticModel, out replacementNode, out issueSpan, cancellationToken);
}
private static bool TryReduceExplicitName(
ExpressionSyntax expression,
SemanticModel semanticModel,
[NotNullWhen(true)] out TypeSyntax? replacementNode,
out TextSpan issueSpan,
CSharpSimplifierOptions options,
CancellationToken cancellationToken)
{
replacementNode = null;
issueSpan = default;
if (expression.ContainsInterleavedDirective(cancellationToken))
return false;
if (expression is MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess)
return TryReduceMemberAccessExpression(memberAccess, semanticModel, out replacementNode, out issueSpan, options, cancellationToken);
if (expression is NameSyntax name)
return NameSimplifier.Instance.TrySimplify(name, semanticModel, options, out replacementNode, out issueSpan, cancellationToken);
return false;
}
private static bool TryReduceMemberAccessExpression(
MemberAccessExpressionSyntax memberAccess,
SemanticModel semanticModel,
[NotNullWhen(true)] out TypeSyntax? replacementNode,
out TextSpan issueSpan,
CSharpSimplifierOptions options,
CancellationToken cancellationToken)
{
replacementNode = null;
issueSpan = default;
if (memberAccess.Name == null || memberAccess.Expression == null)
return false;
// if this node is annotated as being a SpecialType, let's use this information.
if (memberAccess.HasAnnotations(SpecialTypeAnnotation.Kind))
{
var replacementToken = TryGetPredefinedKeywordToken(
semanticModel, SpecialTypeAnnotation.GetSpecialType(memberAccess.GetAnnotations(SpecialTypeAnnotation.Kind).First()));
if (replacementToken != null)
{
replacementNode = CreatePredefinedTypeSyntax(memberAccess, replacementToken.Value);
issueSpan = memberAccess.Span;
return true;
}
}
// See https://github.com/dotnet/roslyn/issues/40974
//
// To be very safe, we only support simplifying code that bound to a symbol without any
// sort of problems. We could potentially relax this in the future. However, we would
// need to be very careful about the implications of us offering to fixup 'broken' code
// in a manner that might end up making things worse or confusing the user.
var symbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, memberAccess);
if (symbol == null)
{
// nameof(X.Y) does not support GetSymbolInfo on X.Y.
// use GetMemberGroup instead to see if we can find a real symbol to use here.
symbol =
memberAccess.IsNameOfArgumentExpression() &&
semanticModel.GetMemberGroup(memberAccess, cancellationToken) is [ISymbol member, ..]
? member
: null;
if (symbol == null)
return false;
}
// if this node is on the left side, we could simplify to aliases
if (!memberAccess.IsRightSideOfDot())
{
// Check if we need to replace this syntax with an alias identifier
if (TryReplaceExpressionWithAlias(
memberAccess, semanticModel, symbol,
cancellationToken, out var aliasReplacement))
{
// get the token text as it appears in source code to preserve e.g. unicode character escaping
var text = aliasReplacement.Name;
var syntaxRef = aliasReplacement.DeclaringSyntaxReferences.FirstOrDefault();
if (syntaxRef != null)
{
var declIdentifier = ((UsingDirectiveSyntax)syntaxRef.GetSyntax(cancellationToken)).Alias!.Name.Identifier;
text = declIdentifier.IsVerbatimIdentifier() ? declIdentifier.ToString()[1..] : declIdentifier.ToString();
}
replacementNode = SyntaxFactory.IdentifierName(
memberAccess.Name.Identifier.CopyAnnotationsTo(SyntaxFactory.Identifier(
memberAccess.GetLeadingTrivia(),
SyntaxKind.IdentifierToken,
text,
aliasReplacement.Name,
memberAccess.GetTrailingTrivia())));
replacementNode = memberAccess.CopyAnnotationsTo(replacementNode);
replacementNode = memberAccess.Name.CopyAnnotationsTo(replacementNode);
issueSpan = memberAccess.Span;
// In case the alias name is the same as the last name of the alias target, we only include
// the left part of the name in the unnecessary span to Not confuse uses.
if (memberAccess.Name.Identifier.ValueText == ((IdentifierNameSyntax)replacementNode).Identifier.ValueText)
{
issueSpan = memberAccess.Expression.Span;
}
return true;
}
// Check if the Expression can be replaced by Predefined Type keyword
if (PreferPredefinedTypeKeywordInMemberAccess(memberAccess, options, semanticModel))
{
if (symbol is INamedTypeSymbol namedType)
{
var keywordToken = TryGetPredefinedKeywordToken(semanticModel, namedType.SpecialType);
if (keywordToken != null)
{
replacementNode = CreatePredefinedTypeSyntax(memberAccess, keywordToken.Value);
replacementNode = replacementNode
.WithAdditionalAnnotations<TypeSyntax>(new SyntaxAnnotation(
nameof(CodeStyleOptions2.PreferIntrinsicPredefinedTypeKeywordInMemberAccess)));
issueSpan = memberAccess.Span; // we want to show the whole expression as unnecessary
return true;
}
}
}
}
// Try to eliminate cases without actually calling CanReplaceWithReducedName. For expressions of the form
// 'this.Name' or 'base.Name', no additional check here is required.
if (!memberAccess.Expression.IsKind(SyntaxKind.BaseExpression))
{
GetReplacementCandidates(
semanticModel,
memberAccess,
symbol,
out var speculativeSymbols,
out var speculativeNamespacesAndTypes);
if (!IsReplacementCandidate(symbol, speculativeSymbols, speculativeNamespacesAndTypes))
{
return false;
}
}
replacementNode = memberAccess.GetNameWithTriviaMoved();
issueSpan = memberAccess.Expression.Span;
return CanReplaceWithMemberAccessName(
memberAccess, semanticModel, symbol, cancellationToken);
}
private static void GetReplacementCandidates(
SemanticModel semanticModel,
MemberAccessExpressionSyntax memberAccess,
ISymbol actualSymbol,
out ImmutableArray<ISymbol> speculativeSymbols,
out ImmutableArray<ISymbol> speculativeNamespacesAndTypes)
{
var containsNamespaceOrTypeSymbol = actualSymbol is INamespaceOrTypeSymbol;
var containsOtherSymbol = !containsNamespaceOrTypeSymbol;
speculativeSymbols = containsOtherSymbol
? semanticModel.LookupSymbols(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText)
: [];
speculativeNamespacesAndTypes = containsNamespaceOrTypeSymbol
? semanticModel.LookupNamespacesAndTypes(memberAccess.SpanStart, name: memberAccess.Name.Identifier.ValueText)
: [];
}
/// <summary>
/// Determines if <paramref name="speculativeSymbols"/> and <paramref name="speculativeNamespacesAndTypes"/>
/// together contain a superset of the symbols in <paramref name="actualSymbol"/>.
/// </summary>
private static bool IsReplacementCandidate(ISymbol actualSymbol, ImmutableArray<ISymbol> speculativeSymbols, ImmutableArray<ISymbol> speculativeNamespacesAndTypes)
{
if (speculativeSymbols.IsEmpty && speculativeNamespacesAndTypes.IsEmpty)
return false;
if (actualSymbol is object)
{
return speculativeSymbols.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance)
|| speculativeNamespacesAndTypes.Contains(actualSymbol, CandidateSymbolEqualityComparer.Instance);
}
return true;
}
private static bool TrySimplify(
ExpressionSyntax expression,
SemanticModel semanticModel,
[NotNullWhen(true)] out ExpressionSyntax? replacementNode,
out TextSpan issueSpan,
CancellationToken cancellationToken)
{
if (!TrySimplifyWorker(expression, semanticModel, out replacementNode, out issueSpan, cancellationToken))
return false;
// Ensure that replacement doesn't change semantics.
var speculationAnalyzer = new SpeculationAnalyzer(expression, replacementNode, semanticModel, cancellationToken);
return !speculationAnalyzer.ReplacementChangesSemantics();
static bool TrySimplifyWorker(
ExpressionSyntax expression,
SemanticModel semanticModel,
[NotNullWhen(true)] out ExpressionSyntax? replacementNode,
out TextSpan issueSpan,
CancellationToken cancellationToken)
{
replacementNode = null;
issueSpan = default;
switch (expression)
{
case MemberAccessExpressionSyntax(SyntaxKind.SimpleMemberAccessExpression) memberAccess:
{
if (IsNonRemovablePartOfDynamicMethodInvocation(semanticModel, memberAccess, cancellationToken))
return false;
if (!TrySimplifyMemberAccessOrQualifiedName(memberAccess.Expression, memberAccess.Name, semanticModel, out var newLeft, out issueSpan))
return false;
// replacement node might not be in it's simplest form, so add simplify annotation to it.
replacementNode = memberAccess.Update(newLeft, memberAccess.OperatorToken, memberAccess.Name)
.WithAdditionalAnnotations(Simplifier.Annotation);
return true;
}
case QualifiedNameSyntax qualifiedName:
{
if (!TrySimplifyMemberAccessOrQualifiedName(qualifiedName.Left, qualifiedName.Right, semanticModel, out var newLeft, out issueSpan))
return false;
// replacement node might not be in it's simplest form, so add simplify annotation to it.
replacementNode = qualifiedName.Update((NameSyntax)newLeft, qualifiedName.DotToken, qualifiedName.Right)
.WithAdditionalAnnotations(Simplifier.Annotation);
return true;
}
}
return false;
}
}
private static bool CanReplaceWithMemberAccessName(
MemberAccessExpressionSyntax memberAccess,
SemanticModel semanticModel,
ISymbol symbol,
CancellationToken cancellationToken)
{
if (!SimplificationHelpers.IsNamespaceOrTypeOrThisParameter(memberAccess.Expression, semanticModel))
return false;
var speculationAnalyzer = new SpeculationAnalyzer(memberAccess, memberAccess.Name, semanticModel, cancellationToken);
if (!speculationAnalyzer.SymbolsForOriginalAndReplacedNodesAreCompatible() ||
speculationAnalyzer.ReplacementChangesSemantics())
{
return false;
}
if (WillConflictWithExistingLocal(memberAccess, memberAccess.Name, semanticModel))
{
return false;
}
if (IsNonRemovablePartOfDynamicMethodInvocation(semanticModel, memberAccess, cancellationToken))
{
return false;
}
if (AccessMethodWithDynamicArgumentInsideStructConstructor(memberAccess, semanticModel))
{
return false;
}
if (memberAccess.Expression.Kind() == SyntaxKind.BaseExpression)
{
var enclosingNamedType = semanticModel.GetEnclosingNamedType(memberAccess.SpanStart, cancellationToken);
if (enclosingNamedType != null &&
!enclosingNamedType.IsSealed &&
symbol != null &&
symbol.IsOverridable())
{
return false;
}
}
return !MemberAccessExpressionSimplifier.ParserWouldTreatReplacementWithNameAsCast(memberAccess);
}
/// <summary>
/// Tells if the member access is dynamically invoked and cannot be reduced. In the case of
/// <c>NS1.NS2.T1.T2.Method(...dynamic...)</c> we can only remove the <c>NS1.NS2</c>
/// portion. The namespace part is not encoded into the IL, but the specific types in
/// <c>T1.T2</c> and cannot be removed.
/// </summary>
private static bool IsNonRemovablePartOfDynamicMethodInvocation(
SemanticModel semanticModel, MemberAccessExpressionSyntax memberAccess, CancellationToken cancellationToken)
{
var ancestorInvocation = memberAccess.FirstAncestorOrSelf<InvocationExpressionSyntax>();
if (ancestorInvocation?.SpanStart == memberAccess.SpanStart)
{
var leftSymbol = semanticModel.GetSymbolInfo(memberAccess.Expression, cancellationToken).GetAnySymbol();
if (leftSymbol is INamedTypeSymbol)
{
var type = semanticModel.GetTypeInfo(memberAccess.GetRequiredParent(), cancellationToken).Type;
if (type?.Kind == SymbolKind.DynamicType)
{
return true;
}
}
}
return false;
}
/*
* Name Reduction, to implicitly mean "this", is possible only after the initialization of all member variables but
* since the check for initialization of all member variable is a lot of work for this simplification we don't simplify
* even if all the member variables are initialized
*/
private static bool AccessMethodWithDynamicArgumentInsideStructConstructor(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel)
{
var constructor = memberAccess.Ancestors().OfType<ConstructorDeclarationSyntax>().SingleOrDefault();
if (constructor == null || constructor.GetRequiredParent().Kind() is not (SyntaxKind.StructDeclaration or SyntaxKind.RecordStructDeclaration))
{
return false;
}
return semanticModel.GetSymbolInfo(memberAccess.Name).CandidateReason == CandidateReason.LateBound;
}
// Note: The caller needs to verify that replacement doesn't change semantics of the original expression.
private static bool TrySimplifyMemberAccessOrQualifiedName(
ExpressionSyntax left,
ExpressionSyntax right,
SemanticModel semanticModel,
[NotNullWhen(true)] out ExpressionSyntax? replacementNode,
out TextSpan issueSpan)
{
replacementNode = null;
issueSpan = default;
if (left != null && right != null)
{
var leftSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, left);
if (leftSymbol is INamedTypeSymbol)
{
var rightSymbol = SimplificationHelpers.GetOriginalSymbolInfo(semanticModel, right);
if (rightSymbol != null && (rightSymbol.IsStatic || rightSymbol.Kind == SymbolKind.NamedType))
{
// Static member access or nested type member access.
if (rightSymbol.ContainingType is { TypeArguments.Length: 0 } containingType &&
!containingType.Equals(leftSymbol))
{
// Don't simplify to a base type if it has the EditorBrowsable attribute on it. This is
// occasionally done in some APIs to have a 'pseudo internal' base type that has functionality,
// which a user is supposed to only access through some 'pseudo public' derived type instead.
if (!containingType.IsEditorBrowsable(hideAdvancedMembers: true, semanticModel.Compilation, includingSourceSymbols: true))
return false;
// We have a static member access or a nested type member access using a more derived type.
// Simplify syntax so as to use accessed member's most immediate containing type instead of the
// derived type.
replacementNode = containingType.GenerateTypeSyntax().WithTriviaFrom(left);
issueSpan = left.Span;
return true;
}
}
}
}
return false;
}
}
|