// 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.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.Shared.Utilities; /// <summary> /// Helper class to analyze the semantic effects of a speculated syntax node replacement on the parenting nodes. /// Given an expression node from a syntax tree and a new expression from a different syntax tree, /// it replaces the expression with the new expression to create a speculated syntax tree. /// It uses the original tree's semantic model to create a speculative semantic model and verifies that /// the syntax replacement doesn't break the semantics of any parenting nodes of the original expression. /// </summary> internal abstract class AbstractSpeculationAnalyzer< TExpressionSyntax, TTypeSyntax, TAttributeSyntax, TArgumentSyntax, TForEachStatementSyntax, TThrowStatementSyntax, TInvocationExpressionSyntax, TConversion> : ISpeculationAnalyzer where TExpressionSyntax : SyntaxNode where TTypeSyntax : TExpressionSyntax where TAttributeSyntax : SyntaxNode where TArgumentSyntax : SyntaxNode where TForEachStatementSyntax : SyntaxNode where TThrowStatementSyntax : SyntaxNode where TInvocationExpressionSyntax : TExpressionSyntax where TConversion : struct { private readonly TExpressionSyntax _newExpressionForReplace; private readonly bool _skipVerificationForReplacedNode; private readonly bool _failOnOverloadResolutionFailuresInOriginalCode; private readonly bool _isNewSemanticModelSpeculativeModel; private SyntaxNode? _lazySemanticRootOfOriginalExpression; private TExpressionSyntax? _lazyReplacedExpression; private SyntaxNode? _lazySemanticRootOfReplacedExpression; private SemanticModel? _lazySpeculativeSemanticModel; private static readonly SymbolEquivalenceComparer s_includeNullabilityComparer = SymbolEquivalenceComparer.Create(distinguishRefFromOut: true, tupleNamesMustMatch: true, ignoreNullableAnnotations: false, objectAndDynamicCompareEqually: false, arrayAndReadOnlySpanCompareEqually: false); private static readonly SymbolEquivalenceComparer s_arrayAndReadOnlySpanCompareEqually = s_includeNullabilityComparer.With(arrayAndReadOnlySpanCompareEqually: true); /// <summary> /// Creates a semantic analyzer for speculative syntax replacement. /// </summary> /// <param name="expression">Original expression to be replaced.</param> /// <param name="newExpression">New expression to replace the original expression.</param> /// <param name="semanticModel">Semantic model of <paramref name="expression"/> node's syntax tree.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <param name="skipVerificationForReplacedNode"> /// True if semantic analysis should be skipped for the replaced node and performed starting from parent of the original and replaced nodes. /// This could be the case when custom verifications are required to be done by the caller or /// semantics of the replaced expression are different from the original expression. /// </param> /// <param name="failOnOverloadResolutionFailuresInOriginalCode"> /// True if semantic analysis should fail when any of the invocation expression ancestors of <paramref name="expression"/> in original code has overload resolution failures. /// </param> public AbstractSpeculationAnalyzer( TExpressionSyntax expression, TExpressionSyntax newExpression, SemanticModel semanticModel, CancellationToken cancellationToken, bool skipVerificationForReplacedNode = false, bool failOnOverloadResolutionFailuresInOriginalCode = false) { OriginalExpression = expression; _newExpressionForReplace = newExpression; OriginalSemanticModel = semanticModel; CancellationToken = cancellationToken; _skipVerificationForReplacedNode = skipVerificationForReplacedNode; _failOnOverloadResolutionFailuresInOriginalCode = failOnOverloadResolutionFailuresInOriginalCode; _isNewSemanticModelSpeculativeModel = true; _lazyReplacedExpression = null; _lazySemanticRootOfOriginalExpression = null; _lazySemanticRootOfReplacedExpression = null; _lazySpeculativeSemanticModel = null; } protected abstract ISyntaxFacts SyntaxFactsService { get; } protected abstract SyntaxNode GetSemanticRootForSpeculation(TExpressionSyntax expression); protected abstract SemanticModel CreateSpeculativeSemanticModel(SyntaxNode originalNode, SyntaxNode nodeToSpeculate, SemanticModel semanticModel); protected abstract bool IsInNamespaceOrTypeContext(TExpressionSyntax node); protected abstract bool ExpressionMightReferenceMember([NotNullWhen(true)] SyntaxNode? node); protected abstract bool CanAccessInstanceMemberThrough(TExpressionSyntax? expression); protected abstract TConversion ClassifyConversion(SemanticModel model, TExpressionSyntax expression, ITypeSymbol targetType); protected abstract TConversion ClassifyConversion(SemanticModel model, ITypeSymbol originalType, ITypeSymbol targetType); protected abstract bool ConversionsAreCompatible(SemanticModel model1, TExpressionSyntax expression1, SemanticModel model2, TExpressionSyntax expression2); protected abstract bool ConversionsAreCompatible(TExpressionSyntax originalExpression, ITypeSymbol originalTargetType, TExpressionSyntax newExpression, ITypeSymbol newTargetType); protected abstract bool IsReferenceConversion(Compilation model, ITypeSymbol sourceType, ITypeSymbol targetType); protected abstract TExpressionSyntax GetForEachStatementExpression(TForEachStatementSyntax forEachStatement); protected abstract bool IsForEachTypeInferred(TForEachStatementSyntax forEachStatement, SemanticModel semanticModel); protected abstract bool ForEachConversionsAreCompatible(SemanticModel originalModel, TForEachStatementSyntax originalForEach, SemanticModel newModel, TForEachStatementSyntax newForEach); protected abstract void GetForEachSymbols( SemanticModel model, TForEachStatementSyntax forEach, out IMethodSymbol getEnumeratorMethod, out ITypeSymbol elementType, out ImmutableArray<ILocalSymbol> localVariables); protected abstract bool ReplacementChangesSemanticsForNodeLanguageSpecific(SyntaxNode currentOriginalNode, SyntaxNode currentReplacedNode, SyntaxNode? previousOriginalNode, SyntaxNode? previousReplacedNode); protected abstract bool IsParenthesizedExpression([NotNullWhen(true)] SyntaxNode? node); protected abstract bool IsNamedArgument(TArgumentSyntax argument); protected abstract string GetNamedArgumentIdentifierValueText(TArgumentSyntax argument); protected abstract TExpressionSyntax GetThrowStatementExpression(TThrowStatementSyntax throwStatement); protected abstract ImmutableArray<TArgumentSyntax> GetArguments(TExpressionSyntax expression); protected abstract TExpressionSyntax GetReceiver(TExpressionSyntax expression); /// <summary> /// Original expression to be replaced. /// </summary> public TExpressionSyntax OriginalExpression { get; } SyntaxNode ISpeculationAnalyzer.OriginalExpression => OriginalExpression; /// <summary> /// First ancestor of <see cref="OriginalExpression"/> which is either a statement, attribute, constructor initializer, /// field initializer, default parameter initializer or type syntax node. /// It serves as the root node for all semantic analysis for this syntax replacement. /// </summary> public SyntaxNode SemanticRootOfOriginalExpression { get { if (_lazySemanticRootOfOriginalExpression == null) { _lazySemanticRootOfOriginalExpression = GetSemanticRootForSpeculation(this.OriginalExpression); RoslynDebug.AssertNotNull(_lazySemanticRootOfOriginalExpression); } return _lazySemanticRootOfOriginalExpression; } } /// <summary> /// Semantic model for the syntax tree corresponding to <see cref="OriginalExpression"/> /// </summary> public SemanticModel OriginalSemanticModel { get; } /// <summary> /// Node which replaces the <see cref="OriginalExpression"/>. /// Note that this node is a cloned version of <see cref="_newExpressionForReplace"/> node, which has been re-parented /// under the node to be speculated, i.e. <see cref="SemanticRootOfReplacedExpression"/>. /// </summary> public TExpressionSyntax ReplacedExpression { get { EnsureReplacedExpressionAndSemanticRoot(); return _lazyReplacedExpression; } } SyntaxNode ISpeculationAnalyzer.ReplacedExpression => ReplacedExpression; /// <summary> /// Node created by replacing <see cref="OriginalExpression"/> under <see cref="SemanticRootOfOriginalExpression"/> node. /// This node is used as the argument to the GetSpeculativeSemanticModel API and serves as the root node for all /// semantic analysis of the speculated tree. /// </summary> public SyntaxNode SemanticRootOfReplacedExpression { get { EnsureReplacedExpressionAndSemanticRoot(); return _lazySemanticRootOfReplacedExpression; } } /// <summary> /// Speculative semantic model used for analyzing the semantics of the new tree. /// </summary> public SemanticModel SpeculativeSemanticModel { get { EnsureSpeculativeSemanticModel(); return _lazySpeculativeSemanticModel; } } public CancellationToken CancellationToken { get; } protected virtual SyntaxNode GetSemanticRootOfReplacedExpression(SyntaxNode semanticRootOfOriginalExpression, TExpressionSyntax annotatedReplacedExpression) => semanticRootOfOriginalExpression.ReplaceNode(this.OriginalExpression, annotatedReplacedExpression); [MemberNotNull(nameof(_lazySemanticRootOfReplacedExpression), nameof(_lazyReplacedExpression))] private void EnsureReplacedExpressionAndSemanticRoot() { if (_lazySemanticRootOfReplacedExpression == null) { // Because the new expression will change identity once we replace the old // expression in its parent, we annotate it here to allow us to get back to // it after replace. var annotation = new SyntaxAnnotation(); var annotatedExpression = _newExpressionForReplace.WithAdditionalAnnotations(annotation); _lazySemanticRootOfReplacedExpression = GetSemanticRootOfReplacedExpression(this.SemanticRootOfOriginalExpression, annotatedExpression); _lazyReplacedExpression = (TExpressionSyntax)_lazySemanticRootOfReplacedExpression.GetAnnotatedNodesAndTokens(annotation).Single().AsNode()!; } else { RoslynDebug.AssertNotNull(_lazyReplacedExpression); } } [Conditional("DEBUG")] protected abstract void ValidateSpeculativeSemanticModel(SemanticModel speculativeSemanticModel, SyntaxNode nodeToSpeculate); [MemberNotNull(nameof(_lazySpeculativeSemanticModel))] private void EnsureSpeculativeSemanticModel() { if (_lazySpeculativeSemanticModel == null) { var nodeToSpeculate = this.SemanticRootOfReplacedExpression; _lazySpeculativeSemanticModel = CreateSpeculativeSemanticModel(this.SemanticRootOfOriginalExpression, nodeToSpeculate, OriginalSemanticModel); ValidateSpeculativeSemanticModel(_lazySpeculativeSemanticModel, nodeToSpeculate); } } #region Semantic comparison helpers protected virtual bool ReplacementIntroducesDisallowedNullType( TExpressionSyntax originalExpression, TExpressionSyntax newExpression, TypeInfo originalTypeInfo, TypeInfo newTypeInfo) { // If the original expression had no type, it's fine for the new one to have no type either. if (originalTypeInfo.Type == null) return false; // If the original had a type, but the new expression doesn't, this is *normally* bad. However, there are // some cases where it is ok (untyped language expressions that have natural conversions to typed // expressions). Subclasses can override this for those cases. if (newTypeInfo.Type == null) return true; // Otherwise, things look ok so far. return false; } protected bool TypesAreCompatible(TExpressionSyntax originalExpression, TExpressionSyntax newExpression) { RoslynDebug.AssertNotNull(originalExpression); Debug.Assert(this.SemanticRootOfOriginalExpression.DescendantNodesAndSelf().Contains(originalExpression)); RoslynDebug.AssertNotNull(newExpression); Debug.Assert(this.SemanticRootOfReplacedExpression.DescendantNodesAndSelf().Contains(newExpression)); var originalTypeInfo = this.OriginalSemanticModel.GetTypeInfo(originalExpression); var newTypeInfo = this.SpeculativeSemanticModel.GetTypeInfo(newExpression); if (SymbolsAreCompatible(originalTypeInfo.Type, newTypeInfo.Type)) return true; // types changed between the old and new expression (specifically, the new type became null, while the // original type was not). That's ok in some circumstance. Check for those and allow in that specific // case. if (originalTypeInfo.Type != null && newTypeInfo.Type == null && !ReplacementIntroducesDisallowedNullType(originalExpression, newExpression, originalTypeInfo, newTypeInfo)) { return true; } return false; } protected bool ConvertedTypesAreCompatible(TExpressionSyntax originalExpression, TExpressionSyntax newExpression) { RoslynDebug.AssertNotNull(originalExpression); Debug.Assert(this.SemanticRootOfOriginalExpression.DescendantNodesAndSelf().Contains(originalExpression)); RoslynDebug.AssertNotNull(newExpression); Debug.Assert(this.SemanticRootOfReplacedExpression.DescendantNodesAndSelf().Contains(newExpression)); var originalTypeInfo = this.OriginalSemanticModel.GetTypeInfo(originalExpression); var newTypeInfo = this.SpeculativeSemanticModel.GetTypeInfo(newExpression); return SymbolsAreCompatible(originalTypeInfo.ConvertedType, newTypeInfo.ConvertedType); } protected bool ImplicitConversionsAreCompatible(TExpressionSyntax originalExpression, TExpressionSyntax newExpression) { RoslynDebug.AssertNotNull(originalExpression); Debug.Assert(this.SemanticRootOfOriginalExpression.DescendantNodesAndSelf().Contains(originalExpression)); RoslynDebug.AssertNotNull(newExpression); Debug.Assert(this.SemanticRootOfReplacedExpression.DescendantNodesAndSelf().Contains(newExpression)); return ConversionsAreCompatible(this.OriginalSemanticModel, originalExpression, this.SpeculativeSemanticModel, newExpression); } private bool ImplicitConversionsAreCompatible(TExpressionSyntax originalExpression, ITypeSymbol originalTargetType, TExpressionSyntax newExpression, ITypeSymbol newTargetType) { RoslynDebug.AssertNotNull(originalExpression); Debug.Assert(this.SemanticRootOfOriginalExpression.DescendantNodesAndSelf().Contains(originalExpression)); RoslynDebug.AssertNotNull(newExpression); Debug.Assert(this.SemanticRootOfReplacedExpression.DescendantNodesAndSelf().Contains(newExpression)); RoslynDebug.AssertNotNull(originalTargetType); RoslynDebug.AssertNotNull(newTargetType); return ConversionsAreCompatible(originalExpression, originalTargetType, newExpression, newTargetType); } protected bool SymbolsAreCompatible(SyntaxNode originalNode, SyntaxNode newNode, bool requireNonNullSymbols = false) { RoslynDebug.AssertNotNull(originalNode); Debug.Assert(this.SemanticRootOfOriginalExpression.DescendantNodesAndSelf().Contains(originalNode)); RoslynDebug.AssertNotNull(newNode); Debug.Assert(this.SemanticRootOfReplacedExpression.DescendantNodesAndSelf().Contains(newNode)); var originalSymbolInfo = this.OriginalSemanticModel.GetSymbolInfo(originalNode); var newSymbolInfo = this.SpeculativeSemanticModel.GetSymbolInfo(newNode); return SymbolInfosAreCompatible(originalSymbolInfo, newSymbolInfo, requireNonNullSymbols); } public static bool SymbolInfosAreCompatible(SymbolInfo originalSymbolInfo, SymbolInfo newSymbolInfo, bool performEquivalenceCheck, bool requireNonNullSymbols = false) { if (originalSymbolInfo.CandidateReason == newSymbolInfo.CandidateReason) { if (SymbolsAreCompatibleCore(originalSymbolInfo.Symbol, newSymbolInfo.Symbol, performEquivalenceCheck, requireNonNullSymbols)) return true; if (originalSymbolInfo.CandidateReason == CandidateReason.MemberGroup) { var candidateLength = originalSymbolInfo.CandidateSymbols.Length; if (candidateLength > 0 && candidateLength == newSymbolInfo.CandidateSymbols.Length) { for (int i = 0, n = candidateLength; i < n; i++) { if (!SymbolsAreCompatibleCore(originalSymbolInfo.CandidateSymbols[i], newSymbolInfo.CandidateSymbols[i], performEquivalenceCheck, requireNonNullSymbols)) return false; } return true; } } } return false; } protected bool SymbolInfosAreCompatible(SymbolInfo originalSymbolInfo, SymbolInfo newSymbolInfo, bool requireNonNullSymbols = false) => SymbolInfosAreCompatible(originalSymbolInfo, newSymbolInfo, performEquivalenceCheck: !_isNewSemanticModelSpeculativeModel, requireNonNullSymbols: requireNonNullSymbols); protected bool SymbolsAreCompatible(ISymbol? symbol, ISymbol? newSymbol, bool requireNonNullSymbols = false) => SymbolsAreCompatibleCore(symbol, newSymbol, performEquivalenceCheck: !_isNewSemanticModelSpeculativeModel, requireNonNullSymbols: requireNonNullSymbols); private static bool SymbolsAreCompatibleCore( ISymbol? symbol, ISymbol? newSymbol, bool performEquivalenceCheck, bool requireNonNullSymbols = false) { if (symbol == null && newSymbol == null) { return !requireNonNullSymbols; } if (symbol == null || newSymbol == null) { return false; } if (symbol.IsReducedExtension()) { symbol = ((IMethodSymbol)symbol).GetConstructedReducedFrom()!; } if (newSymbol.IsReducedExtension()) { newSymbol = ((IMethodSymbol)newSymbol).GetConstructedReducedFrom()!; } // TODO: Lambda function comparison performs syntax equality, hence is non-trivial to compare lambda methods across different compilations. // For now, just assume they are equal. if (symbol.IsAnonymousFunction()) { return newSymbol.IsAnonymousFunction(); } if (performEquivalenceCheck) { // We are comparing symbols across two semantic models (where neither is the speculative model of other one). // We will use the SymbolEquivalenceComparer to check if symbols are equivalent. return CompareAcrossSemanticModels(symbol, newSymbol); } if (s_includeNullabilityComparer.Equals(symbol, newSymbol)) return true; if (symbol is IMethodSymbol methodSymbol && newSymbol is IMethodSymbol newMethodSymbol) { // If we have local functions, we can't use normal symbol equality for them (since that checks locations). // Have to defer to SymbolEquivalence instead. if (methodSymbol.MethodKind == MethodKind.LocalFunction && newMethodSymbol.MethodKind == MethodKind.LocalFunction) return CompareAcrossSemanticModels(methodSymbol, newMethodSymbol); // Handle equivalence of special built-in comparison operators between enum types and // it's underlying enum type. if (methodSymbol.TryGetPredefinedComparisonOperator(out var originalOp) && newMethodSymbol.TryGetPredefinedComparisonOperator(out var newOp) && originalOp == newOp) { var type = methodSymbol.ContainingType; var newType = newMethodSymbol.ContainingType; if (type != null && newType != null) { if (EnumTypesAreCompatible(type, newType) || EnumTypesAreCompatible(newType, type)) { return true; } } } // We consider two method overloads compatible if one takes a T[] array for a particular parameter, and the // other takes a ReadOnlySpan<T> for the same parameter. This is a considered a supported and desirable API // upgrade story for API authors. Specifically, they start with an array-based method, and then add a // sibling ROS method. In that case, the language will prefer the latter when both are applicable. So if // we make a code change that makes the second compatible, then we are ok with that, as the expectation is // that the new method has the same semantics and it is desirable for code to now call that. // // Note: this comparer will check the method kinds, name, containing type, arity, and virtually everything // else about the methods. The only difference it will allow between the methods is that parameter types // can be different if they are an array vs a ReadOnlySpan. if (s_arrayAndReadOnlySpanCompareEqually.Equals(methodSymbol, newMethodSymbol)) return true; } return false; } private static bool CompareAcrossSemanticModels(ISymbol symbol, ISymbol newSymbol) { // SymbolEquivalenceComparer performs Location equality checks for locals, labels, range-variables and local // functions. As we are comparing symbols from different semantic models, locations will differ. Hence // perform minimal checks for these symbol kinds. if (symbol.Kind != newSymbol.Kind) return false; if (symbol is ILocalSymbol localSymbol && newSymbol is ILocalSymbol newLocalSymbol) { return newSymbol.IsImplicitlyDeclared == symbol.IsImplicitlyDeclared && symbol.Name == newSymbol.Name && CompareAcrossSemanticModels(localSymbol.Type, newLocalSymbol.Type); } if (symbol is ILabelSymbol && newSymbol is ILabelSymbol) return symbol.Name == newSymbol.Name; if (symbol is IRangeVariableSymbol && newSymbol is IRangeVariableSymbol) return symbol.Name == newSymbol.Name; if (symbol is IParameterSymbol parameterSymbol && newSymbol is IParameterSymbol newParameterSymbol && parameterSymbol.ContainingSymbol.IsAnonymousOrLocalFunction() && newParameterSymbol.ContainingSymbol.IsAnonymousOrLocalFunction()) { return symbol.Name == newSymbol.Name && parameterSymbol.IsRefOrOut() == newParameterSymbol.IsRefOrOut() && CompareAcrossSemanticModels(parameterSymbol.Type, newParameterSymbol.Type); } if (symbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction } methodSymbol && newSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction } newMethodSymbol) { return symbol.Name == newSymbol.Name && methodSymbol.Parameters.Length == newMethodSymbol.Parameters.Length && CompareAcrossSemanticModels(methodSymbol.ReturnType, newMethodSymbol.ReturnType) && methodSymbol.Parameters.Zip(newMethodSymbol.Parameters, (p1, p2) => (p1, p2)).All( t => CompareAcrossSemanticModels(t.p1, t.p2)); } return SymbolEquivalenceComparer.Instance.Equals(symbol, newSymbol); } private static bool EnumTypesAreCompatible(INamedTypeSymbol type1, INamedTypeSymbol type2) => type1.IsEnumType() && type1.EnumUnderlyingType?.SpecialType == type2.SpecialType; #endregion /// <summary> /// Determines whether performing the given syntax replacement will change the semantics of any parenting expressions /// by performing a bottom up walk from the <see cref="OriginalExpression"/> up to <see cref="SemanticRootOfOriginalExpression"/> /// in the original tree and simultaneously walking bottom up from <see cref="ReplacedExpression"/> up to <see cref="SemanticRootOfReplacedExpression"/> /// in the speculated syntax tree and performing appropriate semantic comparisons. /// </summary> public bool ReplacementChangesSemantics() { if (this.SemanticRootOfOriginalExpression is TTypeSyntax) { var originalType = (TTypeSyntax)this.OriginalExpression; var newType = (TTypeSyntax)this.ReplacedExpression; return ReplacementBreaksTypeResolution(originalType, newType, useSpeculativeModel: false); } return ReplacementChangesSemantics( currentOriginalNode: this.OriginalExpression, currentReplacedNode: this.ReplacedExpression, originalRoot: this.SemanticRootOfOriginalExpression, skipVerificationForCurrentNode: _skipVerificationForReplacedNode); } protected bool ReplacementChangesSemantics(SyntaxNode currentOriginalNode, SyntaxNode currentReplacedNode, SyntaxNode originalRoot, bool skipVerificationForCurrentNode) { if (this.SpeculativeSemanticModel == null) { // This is possible for some broken code scenarios with parse errors, bail out gracefully here. return true; } SyntaxNode? previousOriginalNode = null, previousReplacedNode = null; while (true) { if (!skipVerificationForCurrentNode && ReplacementChangesSemanticsForNode( currentOriginalNode, currentReplacedNode, previousOriginalNode, previousReplacedNode)) { return true; } if (currentOriginalNode == originalRoot) { break; } RoslynDebug.AssertNotNull(currentOriginalNode.Parent); RoslynDebug.AssertNotNull(currentReplacedNode.Parent); previousOriginalNode = currentOriginalNode; previousReplacedNode = currentReplacedNode; currentOriginalNode = currentOriginalNode.Parent; currentReplacedNode = currentReplacedNode.Parent; skipVerificationForCurrentNode = skipVerificationForCurrentNode && IsParenthesizedExpression(currentReplacedNode); } return false; } /// <summary> /// Checks whether the semantic symbols for the <see cref="OriginalExpression"/> and <see cref="ReplacedExpression"/> are non-null and compatible. /// </summary> public bool SymbolsForOriginalAndReplacedNodesAreCompatible() { if (this.SpeculativeSemanticModel == null) { // This is possible for some broken code scenarios with parse errors, bail out gracefully here. return false; } return SymbolsAreCompatible(this.OriginalExpression, this.ReplacedExpression, requireNonNullSymbols: true); } private bool ReplacementChangesSemanticsForNode(SyntaxNode currentOriginalNode, SyntaxNode currentReplacedNode, SyntaxNode? previousOriginalNode, SyntaxNode? previousReplacedNode) { Debug.Assert(previousOriginalNode == null || previousOriginalNode.Parent == currentOriginalNode); Debug.Assert(previousReplacedNode == null || previousReplacedNode.Parent == currentReplacedNode); if (!InvocationsAreCompatible(currentOriginalNode as TInvocationExpressionSyntax, currentReplacedNode as TInvocationExpressionSyntax)) return true; if (ExpressionMightReferenceMember(currentOriginalNode)) { // If replacing the node will result in a change in overload resolution, we won't remove it. var originalExpression = (TExpressionSyntax)currentOriginalNode; var newExpression = (TExpressionSyntax)currentReplacedNode; if (ReplacementBreaksExpression(originalExpression, newExpression)) { return true; } if (ReplacementBreaksSystemObjectMethodResolution(currentOriginalNode, currentReplacedNode, previousOriginalNode, previousReplacedNode)) { return true; } return !ImplicitConversionsAreCompatible(originalExpression, newExpression); } else if (currentOriginalNode is TForEachStatementSyntax originalForEachStatement) { var newForEachStatement = (TForEachStatementSyntax)currentReplacedNode; return ReplacementBreaksForEachStatement(originalForEachStatement, newForEachStatement); } else if (currentOriginalNode is TAttributeSyntax originalAttribute) { var newAttribute = (TAttributeSyntax)currentReplacedNode; return ReplacementBreaksAttribute(originalAttribute, newAttribute); } else if (currentOriginalNode is TThrowStatementSyntax originalThrowStatement) { var newThrowStatement = (TThrowStatementSyntax)currentReplacedNode; return ReplacementBreaksThrowStatement(originalThrowStatement, newThrowStatement); } else if (ReplacementChangesSemanticsForNodeLanguageSpecific(currentOriginalNode, currentReplacedNode, previousOriginalNode, previousReplacedNode)) { return true; } if (currentOriginalNode is TTypeSyntax originalType) { var newType = (TTypeSyntax)currentReplacedNode; return ReplacementBreaksTypeResolution(originalType, newType); } else if (currentOriginalNode is TExpressionSyntax originalExpression) { var newExpression = (TExpressionSyntax)currentReplacedNode; if (!ImplicitConversionsAreCompatible(originalExpression, newExpression)) return true; RoslynDebug.AssertNotNull(originalExpression); Debug.Assert(this.SemanticRootOfOriginalExpression.DescendantNodesAndSelf().Contains(originalExpression)); RoslynDebug.AssertNotNull(newExpression); Debug.Assert(this.SemanticRootOfReplacedExpression.DescendantNodesAndSelf().Contains(newExpression)); var originalTypeInfo = this.OriginalSemanticModel.GetTypeInfo(originalExpression); var newTypeInfo = this.SpeculativeSemanticModel.GetTypeInfo(newExpression); // If we didn't have an error before, but now we got one, that's bad and should block conversion in all cases. if (newTypeInfo.Type.IsErrorType() && !originalTypeInfo.Type.IsErrorType()) return true; if (ReplacementIntroducesDisallowedNullType(originalExpression, newExpression, originalTypeInfo, newTypeInfo)) return true; } return false; } private bool MemberAccessesAreCompatible(TExpressionSyntax? originalExpression, TExpressionSyntax? newExpression) { // If not expressions, nothing to do here. if (originalExpression is null && newExpression is null) return true; var syntaxFacts = this.SyntaxFactsService; // it is legal to go from expr.X to X if expr is either used for static-lookup (e.g. // `MyNs.MyType.MyStaticMember` -> `MyStaticMember`), or if it's used for instance lookup, but only if expr // is `this` or `base`. We will ensure that the 'X' binds to the same symbol. If so, the static case is // fine, as as long as the member is available without issue (e.g. accessibility etc.) then it doesn't // change meaning when switching. The same holds true for an instance method with this/base as that is // allowed to be implicit if it binds to the same exact member. if (syntaxFacts.IsSimpleMemberAccessExpression(originalExpression) && !syntaxFacts.IsSimpleMemberAccessExpression(newExpression)) { // if we don't even have a name after simplification, this is never ok. if (!syntaxFacts.IsSimpleName(newExpression)) return false; // The symbols before/after must be exactly the same (this also ensures that a base access of some // overridden method will be preserved as otherwise the symbols are different). if (!SymbolsAreCompatible(originalExpression, newExpression)) return false; // If static became instance or instance became static, consider that a change in semantics. Note: this // does mean a change from extension->instance call (or vice versa) will be seen as a change in // semantics. We could potentially support this, but we'd have to do the analysis the instance invoked // on and the instance passed as the first parameter are identical. var originalIsStaticAccess = IsStaticAccess(OriginalSemanticModel.GetSymbolInfo(originalExpression, CancellationToken).Symbol); var replacedIsStaticAccess = IsStaticAccess(this.SpeculativeSemanticModel.GetSymbolInfo(newExpression, CancellationToken).Symbol); if (originalIsStaticAccess != replacedIsStaticAccess) return false; // When binding expr.A, if we didn't bind 'expr' binding to a type, namespace, or other static // thing, then we bound to an instance symbol. It's then only ok to remove 'expr' if 'expr' is // this/base. if (!originalIsStaticAccess) { var originalExpressionOfMemberAccess = syntaxFacts.GetExpressionOfMemberAccessExpression(originalExpression); if (!CanAccessInstanceMemberThrough((TExpressionSyntax?)originalExpressionOfMemberAccess)) return false; } } return true; } private static bool IsStaticAccess(ISymbol? symbol) => symbol is INamespaceOrTypeSymbol or { IsStatic: true }; private bool InvocationsAreCompatible(TInvocationExpressionSyntax? originalInvocation, TInvocationExpressionSyntax? newInvocation) { // If not invocations, nothing to do here. if (originalInvocation is null && newInvocation is null) return true; if (originalInvocation is not null) { // Invocations must stay invocations after update. if (newInvocation is null) return false; var syntaxFacts = this.SyntaxFactsService; if (!MemberAccessesAreCompatible( syntaxFacts.GetExpressionOfInvocationExpression(originalInvocation) as TExpressionSyntax, syntaxFacts.GetExpressionOfInvocationExpression(newInvocation) as TExpressionSyntax)) { return false; } // Add more invocation tests here. } // Add more operation tests here. return true; } /// <summary> /// Determine if removing the cast could cause the semantics of System.Object method call to change. /// E.g. Dim b = CStr(1).GetType() is necessary, but the GetType method symbol info resolves to the same with or without the cast. /// </summary> private bool ReplacementBreaksSystemObjectMethodResolution(SyntaxNode currentOriginalNode, SyntaxNode currentReplacedNode, [NotNullWhen(true)] SyntaxNode? previousOriginalNode, [NotNullWhen(true)] SyntaxNode? previousReplacedNode) { if (previousOriginalNode != null && previousReplacedNode != null) { var originalExpressionSymbol = this.OriginalSemanticModel.GetSymbolInfo(currentOriginalNode).Symbol; var replacedExpressionSymbol = this.SpeculativeSemanticModel.GetSymbolInfo(currentReplacedNode).Symbol; if (IsSymbolSystemObjectInstanceMethod(originalExpressionSymbol) && IsSymbolSystemObjectInstanceMethod(replacedExpressionSymbol)) { var previousOriginalType = this.OriginalSemanticModel.GetTypeInfo(previousOriginalNode).Type; var previousReplacedType = this.SpeculativeSemanticModel.GetTypeInfo(previousReplacedNode).Type; if (previousReplacedType != null && previousOriginalType != null) { return !previousReplacedType.InheritsFromOrEquals(previousOriginalType); } } } return false; } /// <summary> /// Determines if the symbol is a non-overridable, non static method on System.Object (e.g. GetType) /// </summary> private static bool IsSymbolSystemObjectInstanceMethod([NotNullWhen(true)] ISymbol? symbol) { return symbol != null && symbol.IsKind(SymbolKind.Method) && symbol.ContainingType.SpecialType == SpecialType.System_Object && !symbol.IsOverridable() && !symbol.IsStaticType(); } private bool ReplacementBreaksAttribute(TAttributeSyntax attribute, TAttributeSyntax newAttribute) { var attributeSym = this.OriginalSemanticModel.GetSymbolInfo(attribute).Symbol; var newAttributeSym = this.SpeculativeSemanticModel.GetSymbolInfo(newAttribute).Symbol; return !SymbolsAreCompatible(attributeSym, newAttributeSym); } private bool ReplacementBreaksForEachStatement(TForEachStatementSyntax forEachStatement, TForEachStatementSyntax newForEachStatement) { var forEachExpression = GetForEachStatementExpression(forEachStatement); if (forEachExpression.IsMissing || !forEachExpression.Span.Contains(OriginalExpression.SpanStart)) { return false; } GetForEachSymbols(this.OriginalSemanticModel, forEachStatement, out var originalGetEnumerator, out var originalElementType, out var originalLocalVariables); GetForEachSymbols(this.SpeculativeSemanticModel, newForEachStatement, out var newGetEnumerator, out var newElementType, out var newLocalVariables); // inferred variable type compatible if (IsForEachTypeInferred(forEachStatement, OriginalSemanticModel)) { if (originalLocalVariables.Length != newLocalVariables.Length) return true; for (int i = 0, n = originalLocalVariables.Length; i < n; i++) { if (!SymbolsAreCompatible(originalLocalVariables[i].Type, newLocalVariables[i].Type)) return true; } } var newForEachExpression = GetForEachStatementExpression(newForEachStatement); if (ReplacementBreaksForEachGetEnumerator(originalGetEnumerator, newGetEnumerator, newForEachExpression) || !ForEachConversionsAreCompatible(this.OriginalSemanticModel, forEachStatement, this.SpeculativeSemanticModel, newForEachStatement) || !SymbolsAreCompatible(originalElementType, newElementType)) { return true; } return false; } private bool ReplacementBreaksForEachGetEnumerator(IMethodSymbol getEnumerator, IMethodSymbol newGetEnumerator, TExpressionSyntax newForEachStatementExpression) { if (getEnumerator == null && newGetEnumerator == null) { return false; } if (getEnumerator == null || newGetEnumerator == null) { return true; } if (getEnumerator.ToSignatureDisplayString() != newGetEnumerator.ToSignatureDisplayString()) { // Note this is likely an interface member from IEnumerable but the new member may be a // GetEnumerator method on a specific type. if (getEnumerator.IsImplementableMember()) { var expressionType = this.SpeculativeSemanticModel.GetTypeInfo(newForEachStatementExpression, CancellationToken).ConvertedType; if (expressionType != null) { var implementationMember = expressionType.FindImplementationForInterfaceMember(getEnumerator); if (implementationMember != null) { if (implementationMember.ToSignatureDisplayString() != newGetEnumerator.ToSignatureDisplayString()) { return false; } } } } return true; } return false; } private bool ReplacementBreaksThrowStatement(TThrowStatementSyntax originalThrowStatement, TThrowStatementSyntax newThrowStatement) { var originalThrowExpression = GetThrowStatementExpression(originalThrowStatement); var originalThrowExpressionType = this.OriginalSemanticModel.GetTypeInfo(originalThrowExpression).Type; var newThrowExpression = GetThrowStatementExpression(newThrowStatement); var newThrowExpressionType = this.SpeculativeSemanticModel.GetTypeInfo(newThrowExpression).Type; // C# language specification requires that type of the expression passed to ThrowStatement is or derives from System.Exception. return originalThrowExpressionType.IsOrDerivesFromExceptionType(this.OriginalSemanticModel.Compilation) != newThrowExpressionType.IsOrDerivesFromExceptionType(this.SpeculativeSemanticModel.Compilation); } private bool ReplacementBreaksTypeResolution(TTypeSyntax type, TTypeSyntax newType, bool useSpeculativeModel = true) { var symbol = this.OriginalSemanticModel.GetSymbolInfo(type).Symbol; ISymbol? newSymbol; if (useSpeculativeModel) { newSymbol = this.SpeculativeSemanticModel.GetSymbolInfo(newType, CancellationToken).Symbol; } else { var bindingOption = IsInNamespaceOrTypeContext(type) ? SpeculativeBindingOption.BindAsTypeOrNamespace : SpeculativeBindingOption.BindAsExpression; newSymbol = this.OriginalSemanticModel.GetSpeculativeSymbolInfo(type.SpanStart, newType, bindingOption).Symbol; } return symbol != null && !SymbolsAreCompatible(symbol, newSymbol); } private static bool IsDelegateInvoke(ISymbol symbol) => symbol is IMethodSymbol { MethodKind: MethodKind.DelegateInvoke }; private static bool IsAnonymousDelegateInvoke(ISymbol symbol) { return IsDelegateInvoke(symbol) && symbol.ContainingType != null && symbol.ContainingType.IsAnonymousType(); } private bool ReplacementBreaksExpression(TExpressionSyntax expression, TExpressionSyntax newExpression) { var originalSymbolInfo = OriginalSemanticModel.GetSymbolInfo(expression); if (_failOnOverloadResolutionFailuresInOriginalCode && originalSymbolInfo.CandidateReason == CandidateReason.OverloadResolutionFailure) { return true; } var newSymbolInfo = this.SpeculativeSemanticModel.GetSymbolInfo(node: newExpression); var symbol = originalSymbolInfo.Symbol; var newSymbol = newSymbolInfo.Symbol; if (SymbolInfosAreCompatible(originalSymbolInfo, newSymbolInfo)) { // Original and new symbols for the invocation expression are compatible. // However, if the symbols are interface members and if the receiver symbol for one of the expressions is a possible ValueType type parameter, // and the other one is not, then there might be a boxing conversion at runtime which causes different runtime behavior. if (symbol.IsImplementableMember()) { if (IsReceiverNonUniquePossibleValueTypeParam(expression, this.OriginalSemanticModel) != IsReceiverNonUniquePossibleValueTypeParam(newExpression, this.SpeculativeSemanticModel)) { return true; } } return false; } if (symbol == null || newSymbol == null || originalSymbolInfo.CandidateReason != newSymbolInfo.CandidateReason) { return true; } if (newSymbol.IsOverride) { for (var overriddenMember = newSymbol.GetOverriddenMember(); overriddenMember != null; overriddenMember = overriddenMember.GetOverriddenMember()) { if (symbol.Equals(overriddenMember)) return !SymbolsHaveCompatibleParameterLists(symbol, newSymbol, expression); } } if (symbol.IsImplementableMember() && IsCompatibleInterfaceMemberImplementation( symbol, newSymbol, expression, newExpression, this.SpeculativeSemanticModel)) { return false; } // Allow speculated invocation expression to bind to a different method symbol if the method's containing type is a delegate type // which has a delegate variance conversion to/from the original method's containing delegate type. if (newSymbol.ContainingType.IsDelegateType() && symbol.ContainingType.IsDelegateType() && IsReferenceConversion(this.OriginalSemanticModel.Compilation, newSymbol.ContainingType, symbol.ContainingType)) { return false; } // Heuristic: If we now bind to an anonymous delegate's invoke method, assume that // this isn't a change in overload resolution. if (IsAnonymousDelegateInvoke(newSymbol)) { return false; } return true; } protected bool ReplacementBreaksCompoundAssignment( TExpressionSyntax originalLeft, TExpressionSyntax originalRight, TExpressionSyntax newLeft, TExpressionSyntax newRight) { var originalTargetType = this.OriginalSemanticModel.GetTypeInfo(originalLeft).Type; if (originalTargetType != null) { var newTargetType = this.SpeculativeSemanticModel.GetTypeInfo(newLeft).Type; return !SymbolsAreCompatible(originalTargetType, newTargetType) || !ImplicitConversionsAreCompatible(originalRight, originalTargetType, newRight, newTargetType!); } return false; } private bool IsCompatibleInterfaceMemberImplementation( ISymbol symbol, ISymbol newSymbol, TExpressionSyntax originalExpression, TExpressionSyntax newExpression, SemanticModel speculativeSemanticModel) { // In general, we don't want to remove casts to interfaces. It may have subtle changes in behavior, // especially if the types in question change in the future. For example, if a type becomes non-sealed or a // new interface impl is introduced, we may subtly break things. // // The only cases where we feel confident enough to elide the cast are: // // 1. When we have an Array/Delegate/Enum. These are such core types, and cannot be changed by teh user, // that we can trust their impls to not change. // 2. We have one of the builtin structs (like int). These are such core types, and cannot be changed by teh // user, that we can trust their impls to not change. // 3. if we have a struct and we know we have a fresh copy of it. In that case, boxing the struct to the // interface doesn't serve any purpose. var newSymbolContainingType = newSymbol.ContainingType; if (newSymbolContainingType == null) return false; var newReceiver = GetReceiver(newExpression); var newReceiverType = newReceiver != null ? speculativeSemanticModel.GetTypeInfo(newReceiver).ConvertedType : newSymbolContainingType; if (newReceiverType == null) return false; var implementationMember = newSymbolContainingType.FindImplementationForInterfaceMember(symbol); if (implementationMember == null) return false; if (!newSymbol.Equals(implementationMember)) return false; if (!SymbolsHaveCompatibleParameterLists(symbol, implementationMember, originalExpression)) return false; if (newReceiverType.IsValueType) { // Presume builtin value types are all immutable, and thus will have the same semantics when you call // interface members on them directly instead of through a boxed copy. if (newReceiverType.SpecialType != SpecialType.None) return true; // For non-builtins, only remove the boxing if we know we have a copy already. return newReceiver != null && IsReceiverUniqueInstance(newReceiver, speculativeSemanticModel); } return newSymbolContainingType.SpecialType is SpecialType.System_Array or SpecialType.System_Delegate or SpecialType.System_Enum or SpecialType.System_String; } private bool IsReceiverNonUniquePossibleValueTypeParam(TExpressionSyntax invocation, SemanticModel semanticModel) { var receiver = GetReceiver(invocation); if (receiver != null) { var receiverType = semanticModel.GetTypeInfo(receiver).Type; if (receiverType.IsKind(SymbolKind.TypeParameter) && !receiverType.IsReferenceType) { return !IsReceiverUniqueInstance(receiver, semanticModel); } } return false; } // Returns true if the given receiver expression for an invocation represents a unique copy of the underlying // object that is not referenced by any other variable. For example, if the receiver expression is produced by a // method call, property, or indexer, then it will be a fresh receiver in the case of value types. private static bool IsReceiverUniqueInstance(TExpressionSyntax receiver, SemanticModel semanticModel) { var receiverSymbol = semanticModel.GetSymbolInfo(receiver).GetAnySymbol(); if (receiverSymbol == null) return false; return receiverSymbol.IsKind(SymbolKind.Method) || receiverSymbol.IsIndexer() || receiverSymbol.IsKind(SymbolKind.Property); } private bool SymbolsHaveCompatibleParameterLists(ISymbol originalSymbol, ISymbol newSymbol, TExpressionSyntax originalInvocation) { if (originalSymbol.IsKind(SymbolKind.Method) || originalSymbol.IsIndexer()) { var specifiedArguments = GetArguments(originalInvocation); if (!specifiedArguments.IsDefault) { var symbolParameters = originalSymbol.GetParameters(); var newSymbolParameters = newSymbol.GetParameters(); return AreCompatibleParameterLists(specifiedArguments, symbolParameters, newSymbolParameters); } } return true; } private bool AreCompatibleParameterLists( ImmutableArray<TArgumentSyntax> specifiedArguments, ImmutableArray<IParameterSymbol> signature1Parameters, ImmutableArray<IParameterSymbol> signature2Parameters) { Debug.Assert(signature1Parameters.Length == signature2Parameters.Length); Debug.Assert(specifiedArguments.Length <= signature1Parameters.Length || (signature1Parameters.Length > 0 && !signature1Parameters.Last().IsParams)); if (signature1Parameters.Length != signature2Parameters.Length) { return false; } // If there aren't any parameters, we're OK. if (signature1Parameters.Length == 0) { return true; } // To ensure that the second parameter list is called in the same way as the // first, we need to use the specified arguments to bail out if... // // * A named argument doesn't have a corresponding parameter in the // in either parameter list, or... // // * A named argument matches a parameter that is in a different position // in the two parameter lists. // // After checking the specified arguments, we walk the unspecified parameters // in both parameter lists to ensure that they have matching default values. var specifiedParameters1 = new List<IParameterSymbol>(); var specifiedParameters2 = new List<IParameterSymbol>(); for (var i = 0; i < specifiedArguments.Length; i++) { var argument = specifiedArguments[i]; // Handle named argument if (IsNamedArgument(argument)) { var name = GetNamedArgumentIdentifierValueText(argument); var parameter1 = signature1Parameters.FirstOrDefault(p => p.Name == name); RoslynDebug.AssertNotNull(parameter1); var parameter2 = signature2Parameters.FirstOrDefault(p => p.Name == name); if (parameter2 == null) { return false; } if (signature1Parameters.IndexOf(parameter1) != signature2Parameters.IndexOf(parameter2)) { return false; } specifiedParameters1.Add(parameter1); specifiedParameters2.Add(parameter2); } else { // otherwise, treat the argument positionally, taking care to properly // handle params parameters. if (i < signature1Parameters.Length) { specifiedParameters1.Add(signature1Parameters[i]); specifiedParameters2.Add(signature2Parameters[i]); } } } // At this point, we can safely assume that specifiedParameters1 and signature2Parameters // contain parameters that appear at the same positions in their respective signatures // because we bailed out if named arguments referred to parameters at different positions. // // Now we walk the unspecified parameters to ensure that they have the same default // values. for (var i = 0; i < signature1Parameters.Length; i++) { var parameter1 = signature1Parameters[i]; if (specifiedParameters1.Contains(parameter1)) { continue; } var parameter2 = signature2Parameters[i]; Debug.Assert(parameter1.HasExplicitDefaultValue, "Expected all unspecified parameter to have default values"); Debug.Assert(parameter1.HasExplicitDefaultValue == parameter2.HasExplicitDefaultValue); if (parameter1.HasExplicitDefaultValue && parameter2.HasExplicitDefaultValue) { if (!object.Equals(parameter2.ExplicitDefaultValue, parameter1.ExplicitDefaultValue)) { return false; } if (object.Equals(parameter1.ExplicitDefaultValue, 0.0)) { RoslynDebug.Assert(object.Equals(parameter2.ExplicitDefaultValue, 0.0)); var isParam1DefaultValueNegativeZero = double.IsNegativeInfinity(1.0 / (double)parameter1.ExplicitDefaultValue); var isParam2DefaultValueNegativeZero = double.IsNegativeInfinity(1.0 / (double)parameter2.ExplicitDefaultValue); if (isParam1DefaultValueNegativeZero != isParam2DefaultValueNegativeZero) { return false; } } } } return true; } protected void GetConversions( TExpressionSyntax originalExpression, ITypeSymbol originalTargetType, TExpressionSyntax newExpression, ITypeSymbol newTargetType, out TConversion? originalConversion, out TConversion? newConversion) { originalConversion = null; newConversion = null; if (this.OriginalSemanticModel.GetTypeInfo(originalExpression).Type != null && this.SpeculativeSemanticModel.GetTypeInfo(newExpression).Type != null) { originalConversion = ClassifyConversion(this.OriginalSemanticModel, originalExpression, originalTargetType); newConversion = ClassifyConversion(this.SpeculativeSemanticModel, newExpression, newTargetType); } else { var originalConvertedTypeSymbol = this.OriginalSemanticModel.GetTypeInfo(originalExpression).ConvertedType; if (originalConvertedTypeSymbol != null) { originalConversion = ClassifyConversion(this.OriginalSemanticModel, originalConvertedTypeSymbol, originalTargetType); } var newConvertedTypeSymbol = this.SpeculativeSemanticModel.GetTypeInfo(newExpression).ConvertedType; if (newConvertedTypeSymbol != null) { newConversion = ClassifyConversion(this.SpeculativeSemanticModel, newConvertedTypeSymbol, newTargetType); } } } } |