|
// 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.Composition;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Differencing;
using Microsoft.CodeAnalysis.EditAndContinue;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue;
internal sealed class CSharpEditAndContinueAnalyzer(Action<SyntaxNode>? testFaultInjector = null) : AbstractEditAndContinueAnalyzer(testFaultInjector)
{
[ExportLanguageServiceFactory(typeof(IEditAndContinueAnalyzer), LanguageNames.CSharp), Shared]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class Factory() : ILanguageServiceFactory
{
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
=> new CSharpEditAndContinueAnalyzer(testFaultInjector: null);
}
#region Syntax Analysis
private enum BlockPart
{
OpenBrace = DefaultStatementPart,
CloseBrace = 1,
}
private enum ForEachPart
{
ForEach = DefaultStatementPart,
VariableDeclaration = 1,
In = 2,
Expression = 3,
}
private enum SwitchExpressionPart
{
WholeExpression = DefaultStatementPart,
// An active statement that covers IL generated for the decision tree:
// <governing-expression> [|switch { <arm>, ..., <arm> }|]
// This active statement is never a leaf active statement (does not correspond to a breakpoint span).
SwitchBody = 1,
}
/// <returns>
/// <see cref="BaseMethodDeclarationSyntax"/> for methods, operators, constructors, destructors and accessors.
/// <see cref="VariableDeclaratorSyntax"/> for field initializers.
/// <see cref="PropertyDeclarationSyntax"/> for property initializers and expression bodies.
/// <see cref="IndexerDeclarationSyntax"/> for indexer expression bodies.
/// <see cref="ArrowExpressionClauseSyntax"/> for getter of an expression-bodied property/indexer.
/// <see cref="CompilationUnitSyntax"/> for top-level statements.
/// <see cref="RecordDeclarationSyntax"/> for record copy-constructors.
/// <see cref="ParameterListSyntax"/> for primary constructors.
/// <see cref="ParameterSyntax"/> for record primary constructor parameters.
/// </returns>
internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, TextSpan activeSpan, out OneOrMany<SyntaxNode> declarations)
{
var current = node;
while (current != null && current != root)
{
switch (current.Kind())
{
case SyntaxKind.ClassDeclaration:
case SyntaxKind.StructDeclaration:
var typeDeclaration = (TypeDeclarationSyntax)current;
// type declaration with primary constructor
if (typeDeclaration.ParameterList != null)
{
declarations = new(typeDeclaration.ParameterList);
return true;
}
break;
case SyntaxKind.RecordDeclaration:
case SyntaxKind.RecordStructDeclaration:
var recordDeclaration = (RecordDeclarationSyntax)current;
declarations = (recordDeclaration.ParameterList != null && activeSpan.OverlapsWith(recordDeclaration.ParameterList.Span))
? new(recordDeclaration.ParameterList) : new(recordDeclaration);
return true;
case SyntaxKind.MethodDeclaration:
case SyntaxKind.ConversionOperatorDeclaration:
case SyntaxKind.OperatorDeclaration:
case SyntaxKind.SetAccessorDeclaration:
case SyntaxKind.InitAccessorDeclaration:
case SyntaxKind.AddAccessorDeclaration:
case SyntaxKind.RemoveAccessorDeclaration:
case SyntaxKind.GetAccessorDeclaration:
case SyntaxKind.ConstructorDeclaration:
case SyntaxKind.DestructorDeclaration:
declarations = new(current);
return true;
case SyntaxKind.PropertyDeclaration:
// int P { get; } = [|initializer|];
Debug.Assert(((PropertyDeclarationSyntax)current).Initializer != null);
declarations = new(current);
return true;
case SyntaxKind.FieldDeclaration:
case SyntaxKind.EventFieldDeclaration:
// Active statements encompassing modifiers or type correspond to the first initialized field.
// [|public static int F = 1|], G = 2;
declarations = new(((BaseFieldDeclarationSyntax)current).Declaration.Variables.First());
return true;
case SyntaxKind.Parameter:
if (current is { Parent.Parent: RecordDeclarationSyntax })
{
declarations = new(current);
return true;
}
break;
case SyntaxKind.VariableDeclarator:
// public static int F = 1, [|G = 2|];
Debug.Assert(current.Parent.IsKind(SyntaxKind.VariableDeclaration));
switch (current.Parent.Parent!.Kind())
{
case SyntaxKind.FieldDeclaration:
case SyntaxKind.EventFieldDeclaration:
declarations = new(current);
return true;
}
current = current.Parent;
break;
case SyntaxKind.ArrowExpressionClause:
// represents getter symbol declaration node of a property/indexer with expression body
if (current.Parent is (kind: SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration))
{
declarations = new(current);
return true;
}
break;
case SyntaxKind.GlobalStatement:
Debug.Assert(current.Parent.IsKind(SyntaxKind.CompilationUnit));
declarations = new(current.Parent);
return true;
}
current = current.Parent;
}
declarations = default;
return false;
}
internal override MemberBody? TryGetDeclarationBody(SyntaxNode node, ISymbol? symbol)
=> SyntaxUtilities.TryGetDeclarationBody(node, symbol);
internal override bool IsDeclarationWithSharedBody(SyntaxNode declaration, ISymbol member)
=> member is IMethodSymbol { AssociatedSymbol: IPropertySymbol property } && property.IsSynthesizedAutoProperty();
protected override bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod)
=> true;
protected override IEnumerable<SyntaxNode> GetVariableUseSites(IEnumerable<SyntaxNode> roots, ISymbol localOrParameter, SemanticModel model, CancellationToken cancellationToken)
{
Debug.Assert(localOrParameter is IParameterSymbol or ILocalSymbol or IRangeVariableSymbol);
// not supported (it's non trivial to find all places where "this" is used):
Debug.Assert(!localOrParameter.IsThisParameter());
return from root in roots
from node in root.DescendantNodesAndSelf()
where node.IsKind(SyntaxKind.IdentifierName)
let nameSyntax = (IdentifierNameSyntax)node
where (string?)nameSyntax.Identifier.Value == localOrParameter.Name &&
(model.GetSymbolInfo(nameSyntax, cancellationToken).Symbol?.Equals(localOrParameter) ?? false)
select node;
}
internal static SyntaxNode FindStatementAndPartner(
TextSpan span,
SyntaxNode body,
SyntaxNode? partnerBody,
out SyntaxNode? partnerStatement,
out int statementPart)
{
var position = span.Start;
if (!body.FullSpan.Contains(position))
{
// invalid position, let's find a labeled node that encompasses the body:
position = body.SpanStart;
}
SyntaxNode node;
if (partnerBody != null)
{
FindLeafNodeAndPartner(body, position, partnerBody, out node, out partnerStatement);
}
else
{
node = body.FindToken(position).Parent!;
partnerStatement = null;
}
while (true)
{
var isBody = node == body || LambdaUtilities.IsLambdaBodyStatementOrExpression(node);
if (isBody || SyntaxComparer.Statement.HasLabel(node))
{
switch (node.Kind())
{
case SyntaxKind.Block:
statementPart = (int)GetStatementPart((BlockSyntax)node, position);
return node;
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
Debug.Assert(!isBody);
statementPart = (int)GetStatementPart((CommonForEachStatementSyntax)node, position);
return node;
case SyntaxKind.DoStatement:
// The active statement of DoStatement node is the while condition,
// which is lexically not the closest breakpoint span (the body is).
// do { ... } [|while (condition);|]
Debug.Assert(position == ((DoStatementSyntax)node).WhileKeyword.SpanStart);
Debug.Assert(!isBody);
goto default;
case SyntaxKind.PropertyDeclaration:
// The active span corresponding to a property declaration is the span corresponding to its initializer (if any),
// not the span corresponding to the accessor.
// int P { [|get;|] } = [|<initializer>|];
Debug.Assert(position == ((PropertyDeclarationSyntax)node).Initializer!.SpanStart);
goto default;
case SyntaxKind.VariableDeclaration:
// VariableDeclaration ::= TypeSyntax CommaSeparatedList(VariableDeclarator)
//
// The compiler places sequence points after each local variable initialization.
// The TypeSyntax is considered to be part of the first sequence span.
Debug.Assert(!isBody);
node = ((VariableDeclarationSyntax)node).Variables.First();
if (partnerStatement != null)
{
partnerStatement = ((VariableDeclarationSyntax)partnerStatement).Variables.First();
}
statementPart = DefaultStatementPart;
return node;
case SyntaxKind.SwitchExpression:
// An active statement that covers IL generated for the decision tree:
// <governing-expression> [|switch { <arm>, ..., <arm> }|]
// This active statement is never a leaf active statement (does not correspond to a breakpoint span).
var switchExpression = (SwitchExpressionSyntax)node;
if (position == switchExpression.SwitchKeyword.SpanStart)
{
Debug.Assert(span.End == switchExpression.CloseBraceToken.Span.End);
statementPart = (int)SwitchExpressionPart.SwitchBody;
return node;
}
// The switch expression itself can be (a part of) an active statement associated with the containing node
// For example, when it is used as a switch arm expression like so:
// <expr> switch { <pattern> [|when <expr> switch { ... }|] ... }
Debug.Assert(position == switchExpression.Span.Start);
if (isBody)
{
goto default;
}
// ascend to parent node:
break;
case SyntaxKind.SwitchExpressionArm:
// An active statement may occur in the when clause and in the arm expression:
// <constant-pattern> [|when <condition>|] => [|<expression>|]
// The former is covered by when-clause node - it's a labeled node.
// The latter isn't enclosed in a distinct labeled syntax node and thus needs to be covered
// by the arm node itself.
Debug.Assert(position == ((SwitchExpressionArmSyntax)node).Expression.SpanStart);
Debug.Assert(!isBody);
goto default;
default:
statementPart = DefaultStatementPart;
return node;
}
}
node = node.Parent!;
if (partnerStatement != null)
{
partnerStatement = partnerStatement.Parent;
}
}
}
private static BlockPart GetStatementPart(BlockSyntax node, int position)
=> position < node.OpenBraceToken.Span.End ? BlockPart.OpenBrace : BlockPart.CloseBrace;
private static TextSpan GetActiveSpan(BlockSyntax node, BlockPart part)
=> part switch
{
BlockPart.OpenBrace => node.OpenBraceToken.Span,
BlockPart.CloseBrace => node.CloseBraceToken.Span,
_ => throw ExceptionUtilities.UnexpectedValue(part),
};
private static ForEachPart GetStatementPart(CommonForEachStatementSyntax node, int position)
=> position < node.OpenParenToken.SpanStart ? ForEachPart.ForEach :
position < node.InKeyword.SpanStart ? ForEachPart.VariableDeclaration :
position < node.Expression.SpanStart ? ForEachPart.In :
ForEachPart.Expression;
private static TextSpan GetActiveSpan(ForEachStatementSyntax node, ForEachPart part)
=> part switch
{
ForEachPart.ForEach => node.ForEachKeyword.Span,
ForEachPart.VariableDeclaration => TextSpan.FromBounds(node.Type.SpanStart, node.Identifier.Span.End),
ForEachPart.In => node.InKeyword.Span,
ForEachPart.Expression => node.Expression.Span,
_ => throw ExceptionUtilities.UnexpectedValue(part),
};
private static TextSpan GetActiveSpan(ForEachVariableStatementSyntax node, ForEachPart part)
=> part switch
{
ForEachPart.ForEach => node.ForEachKeyword.Span,
ForEachPart.VariableDeclaration => TextSpan.FromBounds(node.Variable.SpanStart, node.Variable.Span.End),
ForEachPart.In => node.InKeyword.Span,
ForEachPart.Expression => node.Expression.Span,
_ => throw ExceptionUtilities.UnexpectedValue(part),
};
private static TextSpan GetActiveSpan(SwitchExpressionSyntax node, SwitchExpressionPart part)
=> part switch
{
SwitchExpressionPart.WholeExpression => node.Span,
SwitchExpressionPart.SwitchBody => TextSpan.FromBounds(node.SwitchKeyword.SpanStart, node.CloseBraceToken.Span.End),
_ => throw ExceptionUtilities.UnexpectedValue(part),
};
private static bool AreEquivalentIgnoringLambdaBodies(SyntaxNode left, SyntaxNode right)
{
// usual case:
if (SyntaxFactory.AreEquivalent(left, right))
{
return true;
}
return LambdaUtilities.AreEquivalentIgnoringLambdaBodies(left, right);
}
internal override bool IsClosureScope(SyntaxNode node)
=> LambdaUtilities.IsClosureScope(node);
internal override SyntaxNode GetCapturedParameterScope(SyntaxNode methodOrLambda)
=> methodOrLambda switch
{
// lambda/local function parameter:
AnonymousFunctionExpressionSyntax lambda => lambda.Body,
// ctor parameter captured by a lambda in a ctor initializer:
ConstructorDeclarationSyntax ctor => ctor,
// block statement or arrow expression:
BaseMethodDeclarationSyntax method => method.Body ?? (SyntaxNode?)method.ExpressionBody!,
// primary constructor parameter:
ParameterListSyntax parameters => parameters.Parent!,
// top-level args:
CompilationUnitSyntax top => top,
_ => throw ExceptionUtilities.UnexpectedValue(methodOrLambda)
};
protected override LambdaBody? FindEnclosingLambdaBody(SyntaxNode encompassingAncestor, SyntaxNode node)
{
var current = node;
while (current != encompassingAncestor && current != null)
{
if (LambdaUtilities.IsLambdaBodyStatementOrExpression(current, out var body))
{
return SyntaxUtilities.CreateLambdaBody(body);
}
current = current.Parent;
}
return null;
}
protected override Match<SyntaxNode> ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit)
=> SyntaxComparer.TopLevel.ComputeMatch(oldCompilationUnit, newCompilationUnit);
protected override BidirectionalMap<SyntaxNode>? ComputeParameterMap(SyntaxNode oldDeclaration, SyntaxNode newDeclaration)
=> GetDeclarationParameterList(oldDeclaration) is { } oldParameterList && GetDeclarationParameterList(newDeclaration) is { } newParameterList ?
BidirectionalMap<SyntaxNode>.FromMatch(SyntaxComparer.TopLevel.ComputeMatch(oldParameterList, newParameterList)) : null;
private static SyntaxNode? GetDeclarationParameterList(SyntaxNode declaration)
=> declaration switch
{
ParameterListSyntax parameterList => parameterList,
AccessorDeclarationSyntax { Parent.Parent: IndexerDeclarationSyntax { ParameterList: var list } } => list,
ArrowExpressionClauseSyntax { Parent: { } memberDecl } => GetDeclarationParameterList(memberDecl),
_ => declaration.GetParameterList()
};
internal static Match<SyntaxNode> ComputeBodyMatch(SyntaxNode oldBody, SyntaxNode newBody, IEnumerable<KeyValuePair<SyntaxNode, SyntaxNode>>? knownMatches)
{
SyntaxUtilities.AssertIsBody(oldBody, allowLambda: true);
SyntaxUtilities.AssertIsBody(newBody, allowLambda: true);
if (oldBody is ExpressionSyntax ||
newBody is ExpressionSyntax ||
(oldBody.Parent.IsKind(SyntaxKind.LocalFunctionStatement) && newBody.Parent.IsKind(SyntaxKind.LocalFunctionStatement)))
{
Debug.Assert(oldBody is ExpressionSyntax or BlockSyntax);
Debug.Assert(newBody is ExpressionSyntax or BlockSyntax);
// The matching algorithm requires the roots to match each other.
// Lambda bodies, field/property initializers, and method/property/indexer/operator expression-bodies may also be lambda expressions.
// Say we have oldBody 'x => x' and newBody 'F(x => x + 1)', then
// the algorithm would match 'x => x' to 'F(x => x + 1)' instead of
// matching 'x => x' to 'x => x + 1'.
// We use the parent node as a root:
// - for field/property initializers the root is EqualsValueClause.
// - for member expression-bodies the root is ArrowExpressionClauseSyntax.
// - for block bodies the root is a method/operator/accessor declaration (only happens when matching expression body with a block body)
// - for lambdas the root is a LambdaExpression.
// - for query lambdas the root is the query clause containing the lambda (e.g. where).
// - for local functions the root is LocalFunctionStatement.
static SyntaxNode GetMatchingRoot(SyntaxNode body)
{
var parent = body.Parent!;
// We could apply this change across all ArrowExpressionClause consistently not just for ones with LocalFunctionStatement parents
// but it would require an essential refactoring.
return parent.IsKind(SyntaxKind.ArrowExpressionClause) && parent.Parent.IsKind(SyntaxKind.LocalFunctionStatement) ? parent.Parent : parent;
}
var oldRoot = GetMatchingRoot(oldBody);
var newRoot = GetMatchingRoot(newBody);
var comparer = new SyntaxComparer(oldRoot, newRoot, GetChildNodes(oldRoot, oldBody), GetChildNodes(newRoot, newBody), compareStatementSyntax: true);
return comparer.ComputeMatch(oldRoot, newRoot, knownMatches);
}
return SyntaxComparer.Statement.ComputeMatch(oldBody, newBody, knownMatches);
}
private static IEnumerable<SyntaxNode> GetChildNodes(SyntaxNode root, SyntaxNode body)
{
if (root is LocalFunctionStatementSyntax localFunc)
{
// local functions have multiple children we need to process for matches, but we won't automatically
// descend into them, assuming they're nested, so we override the default behaviour and return
// multiple children
foreach (var attributeList in localFunc.AttributeLists)
{
yield return attributeList;
}
yield return localFunc.ReturnType;
if (localFunc.TypeParameterList is not null)
{
yield return localFunc.TypeParameterList;
}
yield return localFunc.ParameterList;
if (localFunc.Body is not null)
{
yield return localFunc.Body;
}
else if (localFunc.ExpressionBody is not null)
{
// Skip the ArrowExpressionClause that is ExressionBody and just return the expression itself
yield return localFunc.ExpressionBody.Expression;
}
}
else
{
yield return body;
}
}
internal static bool TryMatchActiveStatement(
SyntaxNode oldBody,
SyntaxNode newBody,
SyntaxNode oldStatement,
[NotNullWhen(true)] out SyntaxNode? newStatement)
{
// TODO: Consider mapping an expression body to an equivalent statement expression or return statement and vice versa.
// It would benefit transformations of expression bodies to block bodies of lambdas, methods, operators and properties.
// See https://github.com/dotnet/roslyn/issues/22696
// field initializer, lambda and query expressions:
if (oldStatement == oldBody && !newBody.IsKind(SyntaxKind.Block))
{
newStatement = newBody;
return true;
}
newStatement = null;
return false;
}
#endregion
#region Syntax and Semantic Utils
protected override bool IsNamespaceDeclaration(SyntaxNode node)
=> node is BaseNamespaceDeclarationSyntax;
private static bool IsTypeDeclaration(SyntaxNode node)
=> node is BaseTypeDeclarationSyntax or DelegateDeclarationSyntax;
protected override bool IsCompilationUnitWithGlobalStatements(SyntaxNode node)
=> node is CompilationUnitSyntax unit && unit.ContainsGlobalStatements();
protected override bool IsGlobalStatement(SyntaxNode node)
=> node.IsKind(SyntaxKind.GlobalStatement);
protected override IEnumerable<SyntaxNode> GetTopLevelTypeDeclarations(SyntaxNode compilationUnit)
{
using var _ = ArrayBuilder<SyntaxList<MemberDeclarationSyntax>>.GetInstance(out var stack);
stack.Add(((CompilationUnitSyntax)compilationUnit).Members);
while (stack.Count > 0)
{
var members = stack.Last();
stack.RemoveLast();
foreach (var member in members)
{
if (IsTypeDeclaration(member))
{
yield return member;
}
if (member is BaseNamespaceDeclarationSyntax namespaceMember)
{
stack.Add(namespaceMember.Members);
}
}
}
}
protected override string LineDirectiveKeyword
=> "line";
protected override ushort LineDirectiveSyntaxKind
=> (ushort)SyntaxKind.LineDirectiveTrivia;
protected override IEnumerable<SequenceEdit> GetSyntaxSequenceEdits(ImmutableArray<SyntaxNode> oldNodes, ImmutableArray<SyntaxNode> newNodes)
=> SyntaxComparer.GetSequenceEdits(oldNodes, newNodes);
internal override SyntaxNode EmptyCompilationUnit
=> SyntaxFactory.CompilationUnit();
// there are no experimental features at this time.
internal override bool ExperimentalFeaturesEnabled(SyntaxTree tree)
=> false;
protected override bool StatementLabelEquals(SyntaxNode node1, SyntaxNode node2)
=> SyntaxComparer.Statement.GetLabel(node1) == SyntaxComparer.Statement.GetLabel(node2);
protected override bool TryGetEnclosingBreakpointSpan(SyntaxToken token, out TextSpan span)
=> BreakpointSpans.TryGetClosestBreakpointSpan(token.Parent!, token.SpanStart, minLength: token.Span.Length, out span);
protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span)
{
switch (node.Kind())
{
case SyntaxKind.ArrowExpressionClause:
// Member block body is matched with expression body, so we might be called with statement part of open or closed brace.
Debug.Assert(statementPart is DefaultStatementPart or (int)BlockPart.OpenBrace or (int)BlockPart.CloseBrace);
span = ((ArrowExpressionClauseSyntax)node).Expression.Span;
return true;
case SyntaxKind.Block:
span = GetActiveSpan((BlockSyntax)node, (BlockPart)statementPart);
return true;
case SyntaxKind.ForEachStatement:
span = GetActiveSpan((ForEachStatementSyntax)node, (ForEachPart)statementPart);
return true;
case SyntaxKind.ForEachVariableStatement:
span = GetActiveSpan((ForEachVariableStatementSyntax)node, (ForEachPart)statementPart);
return true;
case SyntaxKind.DoStatement:
// The active statement of DoStatement node is the while condition,
// which is lexically not the closest breakpoint span (the body is).
// do { ... } [|while (condition);|]
Debug.Assert(statementPart == DefaultStatementPart);
var doStatement = (DoStatementSyntax)node;
return BreakpointSpans.TryGetClosestBreakpointSpan(node, doStatement.WhileKeyword.SpanStart, minLength, out span);
case SyntaxKind.PropertyDeclaration:
// The active span corresponding to a property declaration is the span corresponding to its initializer (if any),
// not the span corresponding to the accessor.
// int P { [|get;|] } = [|<initializer>|];
Debug.Assert(statementPart == DefaultStatementPart);
var propertyDeclaration = (PropertyDeclarationSyntax)node;
if (propertyDeclaration.Initializer != null &&
BreakpointSpans.TryGetClosestBreakpointSpan(node, propertyDeclaration.Initializer.SpanStart, minLength, out span))
{
return true;
}
span = default;
return false;
case SyntaxKind.SwitchExpression:
span = GetActiveSpan((SwitchExpressionSyntax)node, (SwitchExpressionPart)statementPart);
return true;
case SyntaxKind.SwitchExpressionArm:
// An active statement may occur in the when clause and in the arm expression:
// <constant-pattern> [|when <condition>|] => [|<expression>|]
// The former is covered by when-clause node - it's a labeled node.
// The latter isn't enclosed in a distinct labeled syntax node and thus needs to be covered
// by the arm node itself.
Debug.Assert(statementPart == DefaultStatementPart);
span = ((SwitchExpressionArmSyntax)node).Expression.Span;
return true;
case SyntaxKind.ParameterList when node.Parent is TypeDeclarationSyntax typeDeclaration:
// The only case when an active statement is a parameter list is an active statement
// for an implicit constructor initializer of a type with primary constructor.
// In that case the span of the active statement starts before the parameter list
// (it includes the type name and type parameters).
span = BreakpointSpans.CreateSpanForImplicitPrimaryConstructorInitializer(typeDeclaration);
return true;
case SyntaxKind.RecordDeclaration:
case SyntaxKind.RecordStructDeclaration:
span = BreakpointSpans.CreateSpanForCopyConstructor((RecordDeclarationSyntax)node);
return true;
default:
// make sure all nodes that use statement parts are handled above:
Debug.Assert(statementPart == DefaultStatementPart);
return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, minLength, out span);
}
}
public static (SyntaxNode node, int part) GetFirstBodyActiveStatement(SyntaxNode memberBody)
=> (memberBody, memberBody.IsKind(SyntaxKind.Block) ? (int)BlockPart.OpenBrace : DefaultStatementPart);
protected override IEnumerable<(SyntaxNode statement, int statementPart)> EnumerateNearStatements(SyntaxNode statement)
{
var direction = +1;
SyntaxNodeOrToken nodeOrToken = statement;
var fieldOrPropertyModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(statement);
while (true)
{
nodeOrToken = (direction < 0) ? nodeOrToken.GetPreviousSibling() : nodeOrToken.GetNextSibling();
if (nodeOrToken.RawKind == 0)
{
var parent = statement.Parent;
if (parent == null)
{
yield break;
}
switch (parent.Kind())
{
case SyntaxKind.Block:
// The next sequence point hit after the last statement of a block is the closing brace:
yield return (parent, (int)(direction > 0 ? BlockPart.CloseBrace : BlockPart.OpenBrace));
break;
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
// The next sequence point hit after the body is the in keyword:
// [|foreach|] ([|variable-declaration|] [|in|] [|expression|]) [|<body>|]
yield return (parent, (int)ForEachPart.In);
break;
}
if (direction > 0)
{
nodeOrToken = statement;
direction = -1;
continue;
}
if (fieldOrPropertyModifiers.HasValue)
{
// We enumerated all members and none of them has an initializer.
// We don't have any better place where to place the span than the initial field.
// Consider: in non-partial classes we could find a single constructor.
// Otherwise, it would be confusing to select one arbitrarily.
yield return (statement, -1);
}
nodeOrToken = statement = parent;
fieldOrPropertyModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(statement);
direction = +1;
yield return (nodeOrToken.AsNode()!, DefaultStatementPart);
}
else
{
var node = nodeOrToken.AsNode();
if (node == null)
{
continue;
}
if (fieldOrPropertyModifiers.HasValue)
{
var nodeModifiers = SyntaxUtilities.TryGetFieldOrPropertyModifiers(node);
if (!nodeModifiers.HasValue ||
nodeModifiers.Value.Any(SyntaxKind.StaticKeyword) != fieldOrPropertyModifiers.Value.Any(SyntaxKind.StaticKeyword))
{
continue;
}
}
switch (node.Kind())
{
case SyntaxKind.Block:
yield return (node, (int)(direction > 0 ? BlockPart.OpenBrace : BlockPart.CloseBrace));
break;
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
yield return (node, (int)ForEachPart.ForEach);
break;
}
yield return (node, DefaultStatementPart);
}
}
}
protected override bool AreEquivalentActiveStatements(SyntaxNode oldStatement, SyntaxNode newStatement, int statementPart)
{
if (oldStatement.Kind() != newStatement.Kind())
{
return false;
}
switch (oldStatement.Kind())
{
case SyntaxKind.Block:
// closing brace of a using statement or a block that contains using local declarations:
if (statementPart == (int)BlockPart.CloseBrace)
{
if (oldStatement.Parent is UsingStatementSyntax oldUsing)
{
return newStatement.Parent is UsingStatementSyntax newUsing &&
AreEquivalentActiveStatements(oldUsing, newUsing);
}
return HasEquivalentUsingDeclarations((BlockSyntax)oldStatement, (BlockSyntax)newStatement);
}
return true;
case SyntaxKind.ConstructorDeclaration:
// The call could only change if the base type of the containing class changed.
return true;
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
// only check the expression, edits in the body and the variable declaration are allowed:
return AreEquivalentActiveStatements((CommonForEachStatementSyntax)oldStatement, (CommonForEachStatementSyntax)newStatement);
case SyntaxKind.IfStatement:
// only check the condition, edits in the body are allowed:
return AreEquivalentActiveStatements((IfStatementSyntax)oldStatement, (IfStatementSyntax)newStatement);
case SyntaxKind.WhileStatement:
// only check the condition, edits in the body are allowed:
return AreEquivalentActiveStatements((WhileStatementSyntax)oldStatement, (WhileStatementSyntax)newStatement);
case SyntaxKind.DoStatement:
// only check the condition, edits in the body are allowed:
return AreEquivalentActiveStatements((DoStatementSyntax)oldStatement, (DoStatementSyntax)newStatement);
case SyntaxKind.SwitchStatement:
return AreEquivalentActiveStatements((SwitchStatementSyntax)oldStatement, (SwitchStatementSyntax)newStatement);
case SyntaxKind.LockStatement:
return AreEquivalentActiveStatements((LockStatementSyntax)oldStatement, (LockStatementSyntax)newStatement);
case SyntaxKind.UsingStatement:
return AreEquivalentActiveStatements((UsingStatementSyntax)oldStatement, (UsingStatementSyntax)newStatement);
// fixed and for statements don't need special handling since the active statement is a variable declaration
default:
return AreEquivalentIgnoringLambdaBodies(oldStatement, newStatement);
}
}
private static bool HasEquivalentUsingDeclarations(BlockSyntax oldBlock, BlockSyntax newBlock)
{
var oldUsingDeclarations = oldBlock.Statements.Where(s => s is LocalDeclarationStatementSyntax l && l.UsingKeyword != default);
var newUsingDeclarations = newBlock.Statements.Where(s => s is LocalDeclarationStatementSyntax l && l.UsingKeyword != default);
return oldUsingDeclarations.SequenceEqual(newUsingDeclarations, AreEquivalentIgnoringLambdaBodies);
}
private static bool AreEquivalentActiveStatements(IfStatementSyntax oldNode, IfStatementSyntax newNode)
{
// only check the condition, edits in the body are allowed:
return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition);
}
private static bool AreEquivalentActiveStatements(WhileStatementSyntax oldNode, WhileStatementSyntax newNode)
{
// only check the condition, edits in the body are allowed:
return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition);
}
private static bool AreEquivalentActiveStatements(DoStatementSyntax oldNode, DoStatementSyntax newNode)
{
// only check the condition, edits in the body are allowed:
return AreEquivalentIgnoringLambdaBodies(oldNode.Condition, newNode.Condition);
}
private static bool AreEquivalentActiveStatements(SwitchStatementSyntax oldNode, SwitchStatementSyntax newNode)
{
// only check the expression, edits in the body are allowed, unless the switch expression contains patterns:
if (!AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression))
{
return false;
}
// Check that switch statement decision tree has not changed.
var hasDecitionTree = oldNode.Sections.Any(s => s.Labels.Any(l => l is CasePatternSwitchLabelSyntax));
return !hasDecitionTree || AreEquivalentSwitchStatementDecisionTrees(oldNode, newNode);
}
private static bool AreEquivalentActiveStatements(LockStatementSyntax oldNode, LockStatementSyntax newNode)
{
// only check the expression, edits in the body are allowed:
return AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression);
}
private static bool AreEquivalentActiveStatements(FixedStatementSyntax oldNode, FixedStatementSyntax newNode)
=> AreEquivalentIgnoringLambdaBodies(oldNode.Declaration, newNode.Declaration);
private static bool AreEquivalentActiveStatements(UsingStatementSyntax oldNode, UsingStatementSyntax newNode)
{
// only check the expression/declaration, edits in the body are allowed:
return AreEquivalentIgnoringLambdaBodies(
(SyntaxNode?)oldNode.Declaration ?? oldNode.Expression!,
(SyntaxNode?)newNode.Declaration ?? newNode.Expression!);
}
private static bool AreEquivalentActiveStatements(CommonForEachStatementSyntax oldNode, CommonForEachStatementSyntax newNode)
{
if (oldNode.Kind() != newNode.Kind() || !AreEquivalentIgnoringLambdaBodies(oldNode.Expression, newNode.Expression))
{
return false;
}
switch (oldNode.Kind())
{
case SyntaxKind.ForEachStatement: return AreEquivalentIgnoringLambdaBodies(((ForEachStatementSyntax)oldNode).Type, ((ForEachStatementSyntax)newNode).Type);
case SyntaxKind.ForEachVariableStatement: return AreEquivalentIgnoringLambdaBodies(((ForEachVariableStatementSyntax)oldNode).Variable, ((ForEachVariableStatementSyntax)newNode).Variable);
default: throw ExceptionUtilities.UnexpectedValue(oldNode.Kind());
}
}
private static bool AreSimilarActiveStatements(CommonForEachStatementSyntax oldNode, CommonForEachStatementSyntax newNode)
{
List<SyntaxToken>? oldTokens = null;
List<SyntaxToken>? newTokens = null;
SyntaxComparer.GetLocalNames(oldNode, ref oldTokens);
SyntaxComparer.GetLocalNames(newNode, ref newTokens);
// A valid foreach statement declares at least one variable.
RoslynDebug.Assert(oldTokens != null);
RoslynDebug.Assert(newTokens != null);
return DeclareSameIdentifiers([.. oldTokens], [.. newTokens]);
}
protected override bool AreEquivalentImpl(SyntaxToken oldToken, SyntaxToken newToken)
=> SyntaxFactory.AreEquivalent(oldToken, newToken);
internal override bool IsInterfaceDeclaration(SyntaxNode node)
=> node.IsKind(SyntaxKind.InterfaceDeclaration);
internal override bool IsRecordDeclaration(SyntaxNode node)
=> node.Kind() is SyntaxKind.RecordDeclaration or SyntaxKind.RecordStructDeclaration;
internal override SyntaxNode? TryGetContainingTypeDeclaration(SyntaxNode node)
=> node is CompilationUnitSyntax ? null : node.Parent!.FirstAncestorOrSelf<BaseTypeDeclarationSyntax>();
internal override bool IsDeclarationWithInitializer(SyntaxNode declaration)
=> declaration is VariableDeclaratorSyntax { Initializer: not null } or PropertyDeclarationSyntax { Initializer: not null };
internal override bool IsPrimaryConstructorDeclaration(SyntaxNode declaration)
=> declaration.Parent is TypeDeclarationSyntax { ParameterList: var parameterList } && parameterList == declaration;
internal override bool IsConstructorWithMemberInitializers(ISymbol symbol, CancellationToken cancellationToken)
{
if (symbol is not IMethodSymbol { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor } method)
{
return false;
}
// static constructor has initializers:
if (method.IsStatic)
{
return true;
}
// If primary constructor is present then no other constructor has member initializers:
if (GetPrimaryConstructor(method.ContainingType, cancellationToken) is { } primaryConstructor)
{
return symbol == primaryConstructor;
}
// Copy-constructor of a record does not have member initializers:
if (method.ContainingType.IsRecord && method.IsCopyConstructor())
{
return false;
}
// Default constructor has initializers unless the type is a struct.
// Struct with member initializers is required to have an explicit constructor.
if (method.IsImplicitlyDeclared)
{
return method.ContainingType.TypeKind != TypeKind.Struct;
}
var ctorInitializer = ((ConstructorDeclarationSyntax)symbol.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken)).Initializer;
if (method.ContainingType.TypeKind == TypeKind.Struct)
{
// constructor of a struct with implicit or this() initializer has member initializers:
return ctorInitializer is null or { ThisOrBaseKeyword: (kind: SyntaxKind.ThisKeyword), ArgumentList.Arguments: [] };
}
else
{
// constructor of a class with implicit or base initializer has member initializers:
return ctorInitializer is null or (kind: SyntaxKind.BaseConstructorInitializer);
}
}
internal override bool IsPartial(INamedTypeSymbol type)
{
var syntaxRefs = type.DeclaringSyntaxReferences;
return syntaxRefs.Length > 1
|| ((BaseTypeDeclarationSyntax)syntaxRefs.Single().GetSyntax()).Modifiers.Any(SyntaxKind.PartialKeyword);
}
protected override SyntaxNode? GetSymbolDeclarationSyntax(ISymbol symbol, Func<ImmutableArray<SyntaxReference>, SyntaxReference?> selector, CancellationToken cancellationToken)
{
// TODO: Workaround for https://github.com/dotnet/roslyn/issues/68510
// symbol.IsImplicitlyDeclared should only be true if symbol.DeclaringSyntaxReferences is non-empty
var syntax = symbol switch
{
IMethodSymbol
{
IsImplicitlyDeclared: true,
ContainingType.IsRecord: true,
Name:
WellKnownMemberNames.PrintMembersMethodName or
WellKnownMemberNames.ObjectEquals or
WellKnownMemberNames.ObjectGetHashCode or
WellKnownMemberNames.ObjectToString or
WellKnownMemberNames.DeconstructMethodName
} => null,
_ => selector(symbol.DeclaringSyntaxReferences)?.GetSyntax(cancellationToken)
};
// Use the parameter list to represent primary constructor declaration.
return symbol.Kind == SymbolKind.Method && syntax is TypeDeclarationSyntax { ParameterList: { } parameterList } ? parameterList : syntax;
}
protected override ISymbol? GetDeclaredSymbol(SemanticModel model, SyntaxNode declaration, CancellationToken cancellationToken)
{
if (IsPrimaryConstructorDeclaration(declaration))
{
Contract.ThrowIfNull(declaration.Parent);
var recordType = (INamedTypeSymbol?)model.GetDeclaredSymbol(declaration.Parent, cancellationToken);
Contract.ThrowIfNull(recordType);
return recordType.InstanceConstructors.Single(ctor => ctor.DeclaringSyntaxReferences is [var syntaxRef] && syntaxRef.GetSyntax(cancellationToken) == declaration.Parent);
}
return model.GetDeclaredSymbol(declaration, cancellationToken);
}
protected override OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> GetEditedSymbols(
EditKind editKind,
SyntaxNode? oldNode,
SyntaxNode? newNode,
SemanticModel? oldModel,
SemanticModel newModel,
CancellationToken cancellationToken)
{
// Chnage in type of a field affects all its variable declarations.
if (oldNode is VariableDeclarationSyntax oldVariableDeclaration && newNode is VariableDeclarationSyntax newVariableDeclaration)
{
return AddFieldSymbolUpdates(oldVariableDeclaration.Variables, newVariableDeclaration.Variables);
}
// Change in attributes or modifiers of a field affects all its variable declarations.
if (oldNode is BaseFieldDeclarationSyntax oldField && newNode is BaseFieldDeclarationSyntax newField)
{
return AddFieldSymbolUpdates(oldField.Declaration.Variables, newField.Declaration.Variables);
}
OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol)> AddFieldSymbolUpdates(SeparatedSyntaxList<VariableDeclaratorSyntax> oldVariables, SeparatedSyntaxList<VariableDeclaratorSyntax> newVariables)
{
Debug.Assert(oldModel != null);
if (oldVariables.Count == 1 && newVariables.Count == 1)
{
return OneOrMany.Create((GetDeclaredSymbol(oldModel, oldVariables[0], cancellationToken), GetDeclaredSymbol(newModel, newVariables[0], cancellationToken)));
}
return OneOrMany.Create(
(from oldVariable in oldVariables
join newVariable in newVariables on oldVariable.Identifier.Text equals newVariable.Identifier.Text
select (GetDeclaredSymbol(oldModel, oldVariable, cancellationToken), GetDeclaredSymbol(newModel, newVariable, cancellationToken))).ToImmutableArray());
}
var oldSymbol = (oldNode != null) ? GetSymbolForEdit(oldNode, oldModel!, cancellationToken) : null;
var newSymbol = (newNode != null) ? GetSymbolForEdit(newNode, newModel, cancellationToken) : null;
return (oldSymbol == null && newSymbol == null)
? OneOrMany<(ISymbol?, ISymbol?)>.Empty
: OneOrMany.Create((oldSymbol, newSymbol));
}
protected override void AddSymbolEdits(
ref TemporaryArray<(ISymbol?, ISymbol?, EditKind)> result,
EditKind editKind,
SyntaxNode? oldNode,
ISymbol? oldSymbol,
SyntaxNode? newNode,
ISymbol? newSymbol,
SemanticModel? oldModel,
SemanticModel newModel,
Match<SyntaxNode> topMatch,
IReadOnlyDictionary<SyntaxNode, EditKind> editMap,
SymbolInfoCache symbolCache,
CancellationToken cancellationToken)
{
if (oldNode is ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax ||
newNode is ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax)
{
var oldContainingMemberOrType = GetParameterContainingMemberOrType(oldNode, newNode, oldModel, topMatch.ReverseMatches, cancellationToken);
var newContainingMemberOrType = GetParameterContainingMemberOrType(newNode, oldNode, newModel, topMatch.Matches, cancellationToken);
var matchingNewContainingMemberOrType = GetSemanticallyMatchingNewSymbol(oldContainingMemberOrType, newContainingMemberOrType, newModel, symbolCache, cancellationToken);
// Create candidate symbol edits to analyze:
// 1) An update of the containing member or type
// Produces an update edit of the containing member or type, or delete & insert edits if the signature changed.
// Reports rude edits for unsupported updates to the body (e.g. to active statements, lambdas, etc.).
// The containing member edit will cover any changes of the name, modifiers and attributes of the parameter.
// 2) Edit of the parameter itself
// Produces semantic edits for any synthesized members generated based on the parameter.
// Reports rude edits for unsupported renames, reoders, attribute and modifer changes.
// Does not result in a semantic edit for the parameter itself.
// 3) Edits of symbols synthesized based on the parameters that have declaring syntax.
// These members need to be analyzed for active statement mapping, type layout, etc.
// E.g. property accessors synthesized for record primary constructor parameters have bodies that may contain active statements.
// If the signature of a property/indexer changed or a parameter of an indexer has been renamed we need to update all its accessors
if (oldContainingMemberOrType is IPropertySymbol oldPropertySymbol &&
newContainingMemberOrType is IPropertySymbol newPropertySymbol &&
(IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol) ||
oldSymbol != null && newSymbol != null && oldSymbol.Name != newSymbol.Name))
{
AddMemberUpdate(ref result, oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, matchingNewContainingMemberOrType);
AddMemberUpdate(ref result, oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, matchingNewContainingMemberOrType);
}
// record primary constructor parameter
if (oldNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax } ||
newNode is ParameterSyntax { Parent.Parent: RecordDeclarationSyntax })
{
Debug.Assert(matchingNewContainingMemberOrType == null);
var oldSynthesizedAutoProperty = (IPropertySymbol?)oldSymbol?.ContainingType.GetMembers(oldSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty());
var newSynthesizedAutoProperty = (IPropertySymbol?)newSymbol?.ContainingType.GetMembers(newSymbol.Name).FirstOrDefault(m => m.IsSynthesizedAutoProperty());
if (oldSynthesizedAutoProperty != null || newSynthesizedAutoProperty != null)
{
result.Add((oldSynthesizedAutoProperty, newSynthesizedAutoProperty, editKind));
result.Add((oldSynthesizedAutoProperty?.GetMethod, newSynthesizedAutoProperty?.GetMethod, editKind));
result.Add((oldSynthesizedAutoProperty?.SetMethod, newSynthesizedAutoProperty?.SetMethod, editKind));
}
}
AddMemberUpdate(ref result, oldContainingMemberOrType, newContainingMemberOrType, matchingNewContainingMemberOrType);
// Any change to a constraint should be analyzed as an update of the type parameter:
var isTypeConstraint = oldNode is TypeParameterConstraintClauseSyntax || newNode is TypeParameterConstraintClauseSyntax;
if (matchingNewContainingMemberOrType != null)
{
// Map parameter to the corresponding semantically matching member.
// Since the signature of the member matches we can direcly map by parameter ordinal.
if (oldSymbol is IParameterSymbol oldParameter)
{
newSymbol = matchingNewContainingMemberOrType.GetParameters()[oldParameter.Ordinal];
}
else if (oldSymbol is ITypeParameterSymbol oldTypeParameter)
{
newSymbol = matchingNewContainingMemberOrType.GetTypeParameters()[oldTypeParameter.Ordinal];
}
}
result.Add((oldSymbol, newSymbol, isTypeConstraint ? EditKind.Update : editKind));
return;
}
switch (editKind)
{
case EditKind.Reorder:
Contract.ThrowIfNull(oldNode);
// When global statements are reordered, we issue an update edit for the synthesized main method, which is what
// oldSymbol and newSymbol will point to.
if (IsGlobalStatement(oldNode))
{
result.Add((oldSymbol, newSymbol, EditKind.Update));
return;
}
// Reordering of data members is only allowed if the layout of the type doesn't change.
// Reordering of other members is a no-op, although the new order won't be reflected in metadata (Reflection will report original order).
result.Add((oldSymbol, newSymbol, EditKind.Reorder));
return;
case EditKind.Update:
Contract.ThrowIfNull(oldNode);
Contract.ThrowIfNull(newNode);
Contract.ThrowIfNull(oldModel);
// Updates of a property/indexer/event node might affect its accessors.
// Return all affected symbols for these updates so that the changes in the accessor bodies get analyzed.
if (oldSymbol is IPropertySymbol oldPropertySymbol && newSymbol is IPropertySymbol newPropertySymbol)
{
// 1) Old or new property/indexer has an expression body:
// int this[...] => expr;
// int this[...] { get => expr; }
// int P => expr;
// int P { get => expr; } = init
//
// Note: An update of a partial property/indexer definition can only affect its attributes. The partial definition does not have a body.
// In that case we do not need to update/analyze the accessors since attribute update has no impact on them.
//
// 2) Property/indexer declarations differ in readonly keyword.
// 3) Property signature changes
// 4) Property name changes
var oldHasExpressionBody = oldNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null };
var newHasExpressionBody = newNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null };
result.Add((oldPropertySymbol, newPropertySymbol, editKind));
if (oldPropertySymbol.GetMethod != null || newPropertySymbol.GetMethod != null)
{
if (oldHasExpressionBody ||
newHasExpressionBody ||
DiffersInReadOnlyModifier(oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod) ||
IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol))
{
result.Add((oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, editKind));
}
}
if (oldPropertySymbol.SetMethod != null || newPropertySymbol.SetMethod != null)
{
if (DiffersInReadOnlyModifier(oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod) ||
IsMemberOrDelegateReplaced(oldPropertySymbol, newPropertySymbol))
{
result.Add((oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, editKind));
}
}
return;
}
if (oldSymbol is IEventSymbol oldEventSymbol && newSymbol is IEventSymbol newEventSymbol)
{
// 1) Event declarations differ in readonly keyword.
// 2) Event signature changes
// 3) Event name changes
result.Add((oldEventSymbol, newEventSymbol, editKind));
if (oldEventSymbol.AddMethod != null || newEventSymbol.AddMethod != null)
{
if (DiffersInReadOnlyModifier(oldEventSymbol.AddMethod, newEventSymbol.AddMethod) ||
IsMemberOrDelegateReplaced(oldEventSymbol, newEventSymbol))
{
result.Add((oldEventSymbol.AddMethod, newEventSymbol.AddMethod, editKind));
}
}
if (oldEventSymbol.RemoveMethod != null || newEventSymbol.RemoveMethod != null)
{
if (DiffersInReadOnlyModifier(oldEventSymbol.RemoveMethod, newEventSymbol.RemoveMethod) ||
IsMemberOrDelegateReplaced(oldEventSymbol, newEventSymbol))
{
result.Add((oldEventSymbol.RemoveMethod, newEventSymbol.RemoveMethod, editKind));
}
}
return;
}
static bool DiffersInReadOnlyModifier(IMethodSymbol? oldMethod, IMethodSymbol? newMethod)
=> oldMethod != null && newMethod != null && oldMethod.IsReadOnly != newMethod.IsReadOnly;
// Update to a type declaration with primary constructor may also need to update
// the primary constructor, copy-constructor and/or synthesized record auto-properties.
// Active statements in bodies of these symbols lay within the type declaration node.
if (oldNode is TypeDeclarationSyntax oldTypeDeclaration &&
newNode is TypeDeclarationSyntax newTypeDeclaration &&
(oldTypeDeclaration.ParameterList != null || newTypeDeclaration.ParameterList != null))
{
if (oldSymbol is not INamedTypeSymbol oldType || newSymbol is not INamedTypeSymbol newType)
{
throw ExceptionUtilities.Unreachable();
}
// the type kind, attributes, constracints may have changed:
result.Add((oldSymbol, newSymbol, EditKind.Update));
var typeNameSpanChanged =
oldTypeDeclaration.Identifier.Span != newTypeDeclaration.Identifier.Span ||
oldTypeDeclaration.TypeParameterList?.Span != newTypeDeclaration.TypeParameterList?.Span;
// primary constructor active span: [|C<T>(...)|]
if (typeNameSpanChanged ||
oldTypeDeclaration.ParameterList?.Span != newTypeDeclaration.ParameterList?.Span)
{
var oldPrimaryConstructor = GetPrimaryConstructor(oldType, cancellationToken);
// The matching constructor might not be specified using primary constructor syntax.
// Let the symbol resolution fill in the other constructor instead of specifying both symbols here.
var newPrimaryConstructor = (oldPrimaryConstructor == null) ? GetPrimaryConstructor(newType, cancellationToken) : null;
result.Add((oldPrimaryConstructor, newPrimaryConstructor, EditKind.Update));
}
// copy constructor active span: [|C<T>|]
if (typeNameSpanChanged && (oldNode.IsKind(SyntaxKind.RecordDeclaration) || newNode.IsKind(SyntaxKind.RecordDeclaration)))
{
var oldCopyConstructor = oldType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor());
var newCopyConstructor = newType.InstanceConstructors.FirstOrDefault(c => c.IsCopyConstructor());
Debug.Assert(oldCopyConstructor != null || newCopyConstructor != null);
result.Add((oldCopyConstructor, newCopyConstructor, EditKind.Update));
}
return;
}
break;
case EditKind.Delete:
case EditKind.Insert:
var node = oldNode ?? newNode;
// If the entire block-bodied property/indexer is deleted/inserted (accessors and the list they are contained in),
// ignore this edit. We will have a semantic edit for the property/indexer itself.
if (node.IsKind(SyntaxKind.GetAccessorDeclaration))
{
Debug.Assert(node.Parent.IsKind(SyntaxKind.AccessorList));
if (HasEdit(editMap, node.Parent, editKind) && !HasEdit(editMap, node.Parent.Parent, editKind))
{
return;
}
}
// Inserting/deleting an expression-bodied property/indexer affects two symbols:
// the property/indexer itself and the getter.
// int this[...] => expr;
// int P => expr;
if (node is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null })
{
var oldGetterSymbol = ((IPropertySymbol?)oldSymbol)?.GetMethod;
var newGetterSymbol = ((IPropertySymbol?)newSymbol)?.GetMethod;
result.Add((oldSymbol, newSymbol, editKind));
result.Add((oldGetterSymbol, newGetterSymbol, editKind));
return;
}
// Inserting/deleting a type parameter constraint should result in an update of the corresponding type parameter symbol:
if (node.IsKind(SyntaxKind.TypeParameterConstraintClause))
{
result.Add((oldSymbol, newSymbol, EditKind.Update));
return;
}
// Inserting/deleting a global statement should result in an update of the implicit main method:
if (node.IsKind(SyntaxKind.GlobalStatement))
{
result.Add((oldSymbol, newSymbol, EditKind.Update));
return;
}
// Inserting/deleting a primary constructor base initializer/base list is an update of the constructor/type,
// not a delete/insert of the constructor/type itself:
if (node is (kind: SyntaxKind.PrimaryConstructorBaseType or SyntaxKind.BaseList))
{
result.Add((oldSymbol, newSymbol, EditKind.Update));
return;
}
break;
case EditKind.Move:
Contract.ThrowIfNull(oldNode);
Contract.ThrowIfNull(newNode);
Contract.ThrowIfNull(oldModel);
Debug.Assert(oldNode.RawKind == newNode.RawKind);
Debug.Assert(SupportsMove(oldNode));
Debug.Assert(SupportsMove(newNode));
if (oldNode.IsKind(SyntaxKind.LocalFunctionStatement))
{
return;
}
result.Add((oldSymbol, newSymbol, editKind));
return;
}
if ((editKind == EditKind.Delete ? oldSymbol : newSymbol) is null)
{
return;
}
result.Add((oldSymbol, newSymbol, editKind));
}
private ISymbol? GetSymbolForEdit(
SyntaxNode node,
SemanticModel model,
CancellationToken cancellationToken)
{
if (node.Kind() is SyntaxKind.UsingDirective or SyntaxKind.NamespaceDeclaration or SyntaxKind.FileScopedNamespaceDeclaration)
{
return null;
}
if (node.IsKind(SyntaxKind.TypeParameterConstraintClause))
{
var constraintClause = (TypeParameterConstraintClauseSyntax)node;
var symbolInfo = model.GetSymbolInfo(constraintClause.Name, cancellationToken);
return symbolInfo.Symbol;
}
// Top level code always lives in a synthesized Main method
if (node.IsKind(SyntaxKind.GlobalStatement))
{
return model.GetEnclosingSymbol(node.SpanStart, cancellationToken);
}
if (node is PrimaryConstructorBaseTypeSyntax primaryCtorBase)
{
return model.GetEnclosingSymbol(primaryCtorBase.ArgumentList.SpanStart, cancellationToken);
}
if (node.IsKind(SyntaxKind.BaseList))
{
Contract.ThrowIfNull(node.Parent);
node = node.Parent;
}
return GetDeclaredSymbol(model, node, cancellationToken);
}
private ISymbol? GetParameterContainingMemberOrType(SyntaxNode? node, SyntaxNode? otherNode, SemanticModel? model, IReadOnlyDictionary<SyntaxNode, SyntaxNode> fromOtherMap, CancellationToken cancellationToken)
{
Debug.Assert(node is null or ParameterSyntax or TypeParameterSyntax or TypeParameterConstraintClauseSyntax);
// parameter list, member, or type declaration:
SyntaxNode? declaration;
if (node == null)
{
Debug.Assert(otherNode != null);
// A parameter-less method/indexer has a parameter list, but non-generic method does not have a type parameter list.
fromOtherMap.TryGetValue(GetContainingDeclaration(otherNode), out declaration);
}
else
{
declaration = GetContainingDeclaration(node);
}
// Parent is a type for primary constructor parameters
if (declaration is BaseParameterListSyntax and not { Parent: TypeDeclarationSyntax })
{
declaration = declaration.Parent;
}
return (declaration != null) ? GetDeclaredSymbol(model!, declaration, cancellationToken) : null;
static SyntaxNode GetContainingDeclaration(SyntaxNode node)
=> node is TypeParameterSyntax ? node.Parent!.Parent! : node!.Parent!;
}
private static bool SupportsMove(SyntaxNode node)
=> node.IsKind(SyntaxKind.LocalFunctionStatement) ||
IsTypeDeclaration(node) ||
node is BaseNamespaceDeclarationSyntax;
internal override Func<SyntaxNode, bool> IsLambda
=> LambdaUtilities.IsLambda;
internal override Func<SyntaxNode, bool> IsNotLambda
=> LambdaUtilities.IsNotLambda;
internal override bool IsLocalFunction(SyntaxNode node)
=> node.IsKind(SyntaxKind.LocalFunctionStatement);
internal override bool IsGenericLocalFunction(SyntaxNode node)
=> node is LocalFunctionStatementSyntax { TypeParameterList: not null };
internal override bool IsNestedFunction(SyntaxNode node)
=> node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax;
internal override bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out LambdaBody? body1, out LambdaBody? body2)
{
if (LambdaUtilities.TryGetLambdaBodies(node, out var bodyNode1, out var bodyNode2))
{
body1 = SyntaxUtilities.CreateLambdaBody(bodyNode1);
body2 = (bodyNode2 != null) ? SyntaxUtilities.CreateLambdaBody(bodyNode2) : null;
return true;
}
body1 = null;
body2 = null;
return false;
}
internal override IMethodSymbol GetLambdaExpressionSymbol(SemanticModel model, SyntaxNode lambdaExpression, CancellationToken cancellationToken)
{
var bodyExpression = LambdaUtilities.GetNestedFunctionBody(lambdaExpression);
return (IMethodSymbol)model.GetRequiredEnclosingSymbol(bodyExpression.SpanStart, cancellationToken);
}
internal override SyntaxNode? GetContainingQueryExpression(SyntaxNode node)
=> node.FirstAncestorOrSelf<QueryExpressionSyntax>();
internal override bool QueryClauseLambdasTypeEquivalent(SemanticModel oldModel, SyntaxNode oldNode, SemanticModel newModel, SyntaxNode newNode, CancellationToken cancellationToken)
{
switch (oldNode.Kind())
{
case SyntaxKind.FromClause:
case SyntaxKind.LetClause:
case SyntaxKind.WhereClause:
case SyntaxKind.OrderByClause:
case SyntaxKind.JoinClause:
var oldQueryClauseInfo = oldModel.GetQueryClauseInfo((QueryClauseSyntax)oldNode, cancellationToken);
var newQueryClauseInfo = newModel.GetQueryClauseInfo((QueryClauseSyntax)newNode, cancellationToken);
return MemberOrDelegateSignaturesEquivalent(oldQueryClauseInfo.CastInfo.Symbol, newQueryClauseInfo.CastInfo.Symbol) &&
MemberOrDelegateSignaturesEquivalent(oldQueryClauseInfo.OperationInfo.Symbol, newQueryClauseInfo.OperationInfo.Symbol);
case SyntaxKind.AscendingOrdering:
case SyntaxKind.DescendingOrdering:
var oldOrderingInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken);
var newOrderingInfo = newModel.GetSymbolInfo(newNode, cancellationToken);
return MemberOrDelegateSignaturesEquivalent(oldOrderingInfo.Symbol, newOrderingInfo.Symbol);
case SyntaxKind.SelectClause:
var oldSelectInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken);
var newSelectInfo = newModel.GetSymbolInfo(newNode, cancellationToken);
// Changing reduced select clause to a non-reduced form or vice versa
// adds/removes a call to Select method, which is a supported change.
return oldSelectInfo.Symbol == null ||
newSelectInfo.Symbol == null ||
MemberOrDelegateSignaturesEquivalent(oldSelectInfo.Symbol, newSelectInfo.Symbol);
case SyntaxKind.GroupClause:
var oldGroupInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken);
var newGroupInfo = newModel.GetSymbolInfo(newNode, cancellationToken);
return GroupBySignatureEquivalent(oldGroupInfo.Symbol as IMethodSymbol, newGroupInfo.Symbol as IMethodSymbol);
default:
return true;
}
}
private static bool GroupBySignatureEquivalent(IMethodSymbol? oldMethod, IMethodSymbol? newMethod)
{
// C# spec paragraph 7.16.2.6 "Groupby clauses":
//
// A query expression of the form
// from x in e group v by k
// is translated into
// (e).GroupBy(x => k, x => v)
// except when v is the identifier x, the translation is
// (e).GroupBy(x => k)
//
// Possible signatures:
// C<G<K, T>> GroupBy<K>(Func<T, K> keySelector);
// C<G<K, E>> GroupBy<K, E>(Func<T, K> keySelector, Func<T, E> elementSelector);
if (oldMethod == newMethod)
{
return true;
}
if (oldMethod == null || newMethod == null)
{
return false;
}
if (!TypesEquivalent(oldMethod.ReturnType, newMethod.ReturnType, exact: false))
{
return false;
}
var oldParameters = oldMethod.Parameters;
var newParameters = newMethod.Parameters;
Debug.Assert(oldParameters.Length is 1 or 2);
Debug.Assert(newParameters.Length is 1 or 2);
// The types of the lambdas have to be the same if present.
// The element selector may be added/removed.
if (!ParameterTypesEquivalent(oldParameters[0], newParameters[0], exact: false))
{
return false;
}
if (oldParameters.Length == newParameters.Length && newParameters.Length == 2)
{
return ParameterTypesEquivalent(oldParameters[1], newParameters[1], exact: false);
}
return true;
}
#endregion
#region Diagnostic Info
protected override SymbolDisplayFormat ErrorDisplayFormat => SymbolDisplayFormat.CSharpErrorMessageFormat;
protected override TextSpan? TryGetDiagnosticSpan(SyntaxNode node, EditKind editKind)
=> TryGetDiagnosticSpanImpl(node, editKind);
internal static new TextSpan GetDiagnosticSpan(SyntaxNode node, EditKind editKind)
=> TryGetDiagnosticSpanImpl(node, editKind) ?? node.Span;
private static TextSpan? TryGetDiagnosticSpanImpl(SyntaxNode node, EditKind editKind)
=> TryGetDiagnosticSpanImpl(node.Kind(), node, editKind);
// internal for testing; kind is passed explicitly for testing as well
internal static TextSpan? TryGetDiagnosticSpanImpl(SyntaxKind kind, SyntaxNode node, EditKind editKind)
{
switch (kind)
{
case SyntaxKind.CompilationUnit:
var unit = (CompilationUnitSyntax)node;
// When deleting something from a compilation unit we just report diagnostics for the last global statement
var globalStatements = unit.Members.OfType<GlobalStatementSyntax>();
var globalNode =
(editKind == EditKind.Delete ? globalStatements.LastOrDefault() : globalStatements.FirstOrDefault()) ??
unit.ChildNodes().FirstOrDefault();
if (globalNode == null)
{
return null;
}
return GetDiagnosticSpan(globalNode, editKind);
case SyntaxKind.GlobalStatement:
return node.Span;
case SyntaxKind.ExternAliasDirective:
case SyntaxKind.UsingDirective:
return node.Span;
case SyntaxKind.NamespaceDeclaration:
case SyntaxKind.FileScopedNamespaceDeclaration:
var ns = (BaseNamespaceDeclarationSyntax)node;
return TextSpan.FromBounds(ns.NamespaceKeyword.SpanStart, ns.Name.Span.End);
case SyntaxKind.ClassDeclaration:
case SyntaxKind.StructDeclaration:
case SyntaxKind.InterfaceDeclaration:
case SyntaxKind.RecordDeclaration:
case SyntaxKind.RecordStructDeclaration:
var typeDeclaration = (TypeDeclarationSyntax)node;
return GetDiagnosticSpan(typeDeclaration.Modifiers, typeDeclaration.Keyword,
typeDeclaration.TypeParameterList ?? (SyntaxNodeOrToken)typeDeclaration.Identifier);
case SyntaxKind.BaseList:
var baseList = (BaseListSyntax)node;
return baseList.Types.Span;
case SyntaxKind.EnumDeclaration:
var enumDeclaration = (EnumDeclarationSyntax)node;
return GetDiagnosticSpan(enumDeclaration.Modifiers, enumDeclaration.EnumKeyword, enumDeclaration.Identifier);
case SyntaxKind.DelegateDeclaration:
var delegateDeclaration = (DelegateDeclarationSyntax)node;
return GetDiagnosticSpan(delegateDeclaration.Modifiers, delegateDeclaration.DelegateKeyword, delegateDeclaration.ParameterList);
case SyntaxKind.FieldDeclaration:
var fieldDeclaration = (BaseFieldDeclarationSyntax)node;
return GetDiagnosticSpan(fieldDeclaration.Modifiers, fieldDeclaration.Declaration, fieldDeclaration.Declaration);
case SyntaxKind.EventFieldDeclaration:
var eventFieldDeclaration = (EventFieldDeclarationSyntax)node;
return GetDiagnosticSpan(eventFieldDeclaration.Modifiers, eventFieldDeclaration.EventKeyword, eventFieldDeclaration.Declaration);
case SyntaxKind.VariableDeclaration:
return TryGetDiagnosticSpanImpl(node.Parent!, editKind);
case SyntaxKind.VariableDeclarator:
return node.Span;
case SyntaxKind.MethodDeclaration:
var methodDeclaration = (MethodDeclarationSyntax)node;
return GetDiagnosticSpan(methodDeclaration.Modifiers, methodDeclaration.ReturnType, methodDeclaration.ParameterList);
case SyntaxKind.ConversionOperatorDeclaration:
var conversionOperatorDeclaration = (ConversionOperatorDeclarationSyntax)node;
return GetDiagnosticSpan(conversionOperatorDeclaration.Modifiers, conversionOperatorDeclaration.ImplicitOrExplicitKeyword, conversionOperatorDeclaration.ParameterList);
case SyntaxKind.OperatorDeclaration:
var operatorDeclaration = (OperatorDeclarationSyntax)node;
return GetDiagnosticSpan(operatorDeclaration.Modifiers, operatorDeclaration.ReturnType, operatorDeclaration.ParameterList);
case SyntaxKind.ConstructorDeclaration:
var constructorDeclaration = (ConstructorDeclarationSyntax)node;
return GetDiagnosticSpan(constructorDeclaration.Modifiers, constructorDeclaration.Identifier, constructorDeclaration.ParameterList);
case SyntaxKind.DestructorDeclaration:
var destructorDeclaration = (DestructorDeclarationSyntax)node;
return GetDiagnosticSpan(destructorDeclaration.Modifiers, destructorDeclaration.TildeToken, destructorDeclaration.ParameterList);
case SyntaxKind.PropertyDeclaration:
var propertyDeclaration = (PropertyDeclarationSyntax)node;
return GetDiagnosticSpan(propertyDeclaration.Modifiers, propertyDeclaration.Type, propertyDeclaration.Identifier);
case SyntaxKind.IndexerDeclaration:
var indexerDeclaration = (IndexerDeclarationSyntax)node;
return GetDiagnosticSpan(indexerDeclaration.Modifiers, indexerDeclaration.Type, indexerDeclaration.ParameterList);
case SyntaxKind.EventDeclaration:
var eventDeclaration = (EventDeclarationSyntax)node;
return GetDiagnosticSpan(eventDeclaration.Modifiers, eventDeclaration.EventKeyword, eventDeclaration.Identifier);
case SyntaxKind.EnumMemberDeclaration:
return node.Span;
case SyntaxKind.GetAccessorDeclaration:
case SyntaxKind.SetAccessorDeclaration:
case SyntaxKind.InitAccessorDeclaration:
case SyntaxKind.AddAccessorDeclaration:
case SyntaxKind.RemoveAccessorDeclaration:
case SyntaxKind.UnknownAccessorDeclaration:
var accessorDeclaration = (AccessorDeclarationSyntax)node;
return GetDiagnosticSpan(accessorDeclaration.Modifiers, accessorDeclaration.Keyword, accessorDeclaration.Keyword);
case SyntaxKind.TypeParameterConstraintClause:
var constraint = (TypeParameterConstraintClauseSyntax)node;
return TextSpan.FromBounds(constraint.WhereKeyword.SpanStart, constraint.Constraints.Last().Span.End);
case SyntaxKind.TypeParameter:
var typeParameter = (TypeParameterSyntax)node;
return typeParameter.Identifier.Span;
case SyntaxKind.AccessorList:
case SyntaxKind.TypeParameterList:
case SyntaxKind.ParameterList:
case SyntaxKind.BracketedParameterList:
if (editKind == EditKind.Delete)
{
return TryGetDiagnosticSpanImpl(node.Parent!, editKind);
}
else
{
return node.Span;
}
case SyntaxKind.Parameter:
var parameter = (ParameterSyntax)node;
// Lambda parameters don't have types or modifiers, so the parameter is the only node
var startNode = parameter.Type ?? (SyntaxNode)parameter;
return GetDiagnosticSpan(parameter.Modifiers, startNode, parameter);
case SyntaxKind.PrimaryConstructorBaseType:
case SyntaxKind.AttributeList:
case SyntaxKind.Attribute:
return node.Span;
case SyntaxKind.ArrowExpressionClause:
return TryGetDiagnosticSpanImpl(node.Parent!, editKind);
// We only need a diagnostic span if reporting an error for a child statement.
// The following statements may have child statements.
case SyntaxKind.Block:
return ((BlockSyntax)node).OpenBraceToken.Span;
case SyntaxKind.UsingStatement:
var usingStatement = (UsingStatementSyntax)node;
return TextSpan.FromBounds(usingStatement.UsingKeyword.SpanStart, usingStatement.CloseParenToken.Span.End);
case SyntaxKind.FixedStatement:
var fixedStatement = (FixedStatementSyntax)node;
return TextSpan.FromBounds(fixedStatement.FixedKeyword.SpanStart, fixedStatement.CloseParenToken.Span.End);
case SyntaxKind.LockStatement:
var lockStatement = (LockStatementSyntax)node;
return TextSpan.FromBounds(lockStatement.LockKeyword.SpanStart, lockStatement.CloseParenToken.Span.End);
case SyntaxKind.StackAllocArrayCreationExpression:
return ((StackAllocArrayCreationExpressionSyntax)node).StackAllocKeyword.Span;
case SyntaxKind.ImplicitStackAllocArrayCreationExpression:
return ((ImplicitStackAllocArrayCreationExpressionSyntax)node).StackAllocKeyword.Span;
case SyntaxKind.TryStatement:
return ((TryStatementSyntax)node).TryKeyword.Span;
case SyntaxKind.CatchClause:
return ((CatchClauseSyntax)node).CatchKeyword.Span;
case SyntaxKind.CatchDeclaration:
case SyntaxKind.CatchFilterClause:
return node.Span;
case SyntaxKind.FinallyClause:
return ((FinallyClauseSyntax)node).FinallyKeyword.Span;
case SyntaxKind.IfStatement:
var ifStatement = (IfStatementSyntax)node;
return TextSpan.FromBounds(ifStatement.IfKeyword.SpanStart, ifStatement.CloseParenToken.Span.End);
case SyntaxKind.ElseClause:
return ((ElseClauseSyntax)node).ElseKeyword.Span;
case SyntaxKind.SwitchStatement:
var switchStatement = (SwitchStatementSyntax)node;
return TextSpan.FromBounds(switchStatement.SwitchKeyword.SpanStart,
(switchStatement.CloseParenToken != default) ? switchStatement.CloseParenToken.Span.End : switchStatement.Expression.Span.End);
case SyntaxKind.SwitchSection:
return ((SwitchSectionSyntax)node).Labels.Last().Span;
case SyntaxKind.WhileStatement:
var whileStatement = (WhileStatementSyntax)node;
return TextSpan.FromBounds(whileStatement.WhileKeyword.SpanStart, whileStatement.CloseParenToken.Span.End);
case SyntaxKind.DoStatement:
return ((DoStatementSyntax)node).DoKeyword.Span;
case SyntaxKind.ForStatement:
var forStatement = (ForStatementSyntax)node;
return TextSpan.FromBounds(forStatement.ForKeyword.SpanStart, forStatement.CloseParenToken.Span.End);
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
var commonForEachStatement = (CommonForEachStatementSyntax)node;
return TextSpan.FromBounds(
(commonForEachStatement.AwaitKeyword.Span.Length > 0) ? commonForEachStatement.AwaitKeyword.SpanStart : commonForEachStatement.ForEachKeyword.SpanStart,
commonForEachStatement.CloseParenToken.Span.End);
case SyntaxKind.LabeledStatement:
return ((LabeledStatementSyntax)node).Identifier.Span;
case SyntaxKind.CheckedStatement:
case SyntaxKind.UncheckedStatement:
return ((CheckedStatementSyntax)node).Keyword.Span;
case SyntaxKind.UnsafeStatement:
return ((UnsafeStatementSyntax)node).UnsafeKeyword.Span;
case SyntaxKind.LocalFunctionStatement:
var lfd = (LocalFunctionStatementSyntax)node;
return lfd.Identifier.Span;
case SyntaxKind.YieldBreakStatement:
case SyntaxKind.YieldReturnStatement:
case SyntaxKind.ReturnStatement:
case SyntaxKind.ThrowStatement:
case SyntaxKind.ExpressionStatement:
case SyntaxKind.EmptyStatement:
case SyntaxKind.GotoStatement:
case SyntaxKind.GotoCaseStatement:
case SyntaxKind.GotoDefaultStatement:
case SyntaxKind.BreakStatement:
case SyntaxKind.ContinueStatement:
return node.Span;
case SyntaxKind.LocalDeclarationStatement:
var localDeclarationStatement = (LocalDeclarationStatementSyntax)node;
return CombineSpans(localDeclarationStatement.AwaitKeyword.Span, localDeclarationStatement.UsingKeyword.Span, node.Span);
case SyntaxKind.AwaitExpression:
return ((AwaitExpressionSyntax)node).AwaitKeyword.Span;
case SyntaxKind.AnonymousObjectCreationExpression:
return ((AnonymousObjectCreationExpressionSyntax)node).NewKeyword.Span;
case SyntaxKind.ParenthesizedLambdaExpression:
var parenthesizedLambda = (ParenthesizedLambdaExpressionSyntax)node;
return CombineSpans(parenthesizedLambda.ReturnType?.Span ?? default, parenthesizedLambda.ParameterList.Span, defaultSpan: default);
case SyntaxKind.SimpleLambdaExpression:
return ((SimpleLambdaExpressionSyntax)node).Parameter.Span;
case SyntaxKind.AnonymousMethodExpression:
return ((AnonymousMethodExpressionSyntax)node).DelegateKeyword.Span;
case SyntaxKind.QueryExpression:
return ((QueryExpressionSyntax)node).FromClause.FromKeyword.Span;
case SyntaxKind.QueryBody:
var queryBody = (QueryBodySyntax)node;
return TryGetDiagnosticSpanImpl(queryBody.Clauses.FirstOrDefault() ?? queryBody.Parent!, editKind);
case SyntaxKind.QueryContinuation:
return ((QueryContinuationSyntax)node).IntoKeyword.Span;
case SyntaxKind.FromClause:
return ((FromClauseSyntax)node).FromKeyword.Span;
case SyntaxKind.JoinClause:
return ((JoinClauseSyntax)node).JoinKeyword.Span;
case SyntaxKind.JoinIntoClause:
return ((JoinIntoClauseSyntax)node).IntoKeyword.Span;
case SyntaxKind.LetClause:
return ((LetClauseSyntax)node).LetKeyword.Span;
case SyntaxKind.WhereClause:
return ((WhereClauseSyntax)node).WhereKeyword.Span;
case SyntaxKind.OrderByClause:
return ((OrderByClauseSyntax)node).OrderByKeyword.Span;
case SyntaxKind.AscendingOrdering:
case SyntaxKind.DescendingOrdering:
return node.Span;
case SyntaxKind.SelectClause:
return ((SelectClauseSyntax)node).SelectKeyword.Span;
case SyntaxKind.GroupClause:
return ((GroupClauseSyntax)node).GroupKeyword.Span;
case SyntaxKind.IsPatternExpression:
case SyntaxKind.TupleType:
case SyntaxKind.TupleExpression:
case SyntaxKind.DeclarationExpression:
case SyntaxKind.RefType:
case SyntaxKind.RefExpression:
case SyntaxKind.DeclarationPattern:
case SyntaxKind.SimpleAssignmentExpression:
case SyntaxKind.WhenClause:
case SyntaxKind.SingleVariableDesignation:
case SyntaxKind.CasePatternSwitchLabel:
return node.Span;
case SyntaxKind.SwitchExpression:
return ((SwitchExpressionSyntax)node).SwitchKeyword.Span;
case SyntaxKind.SwitchExpressionArm:
return ((SwitchExpressionArmSyntax)node).EqualsGreaterThanToken.Span;
default:
return null;
}
}
private static TextSpan GetDiagnosticSpan(SyntaxTokenList modifiers, SyntaxNodeOrToken start, SyntaxNodeOrToken end)
=> TextSpan.FromBounds((modifiers.Count != 0) ? modifiers.First().SpanStart : start.SpanStart, end.Span.End);
private static TextSpan CombineSpans(TextSpan first, TextSpan second, TextSpan defaultSpan)
=> (first.Length > 0 && second.Length > 0) ? TextSpan.FromBounds(first.Start, second.End) : (first.Length > 0) ? first : (second.Length > 0) ? second : defaultSpan;
internal override TextSpan GetLambdaParameterDiagnosticSpan(SyntaxNode lambda, int ordinal)
{
Debug.Assert(ordinal >= 0);
switch (lambda.Kind())
{
case SyntaxKind.ParenthesizedLambdaExpression:
return ((ParenthesizedLambdaExpressionSyntax)lambda).ParameterList.Parameters[ordinal].Identifier.Span;
case SyntaxKind.SimpleLambdaExpression:
Debug.Assert(ordinal == 0);
return ((SimpleLambdaExpressionSyntax)lambda).Parameter.Identifier.Span;
case SyntaxKind.AnonymousMethodExpression:
// since we are given a parameter ordinal there has to be a parameter list:
return ((AnonymousMethodExpressionSyntax)lambda).ParameterList!.Parameters[ordinal].Identifier.Span;
default:
return lambda.Span;
}
}
internal override string GetDisplayName(INamedTypeSymbol symbol)
=> symbol.TypeKind switch
{
TypeKind.Struct => symbol.IsRecord ? CSharpFeaturesResources.record_struct : FeaturesResources.struct_,
TypeKind.Class => symbol.IsRecord ? CSharpFeaturesResources.record_ : FeaturesResources.class_,
_ => base.GetDisplayName(symbol)
};
internal override string GetDisplayName(IEventSymbol symbol)
=> symbol.AddMethod?.IsImplicitlyDeclared != false ? CSharpFeaturesResources.event_field : base.GetDisplayName(symbol);
internal override string GetDisplayName(IPropertySymbol symbol)
=> symbol.IsIndexer ? CSharpFeaturesResources.indexer : base.GetDisplayName(symbol);
internal override string GetDisplayName(IMethodSymbol symbol)
=> symbol.MethodKind switch
{
MethodKind.PropertyGet => symbol.AssociatedSymbol is IPropertySymbol { IsIndexer: true } ? CSharpFeaturesResources.indexer_getter : CSharpFeaturesResources.property_getter,
MethodKind.PropertySet => symbol.AssociatedSymbol is IPropertySymbol { IsIndexer: true } ? CSharpFeaturesResources.indexer_setter : CSharpFeaturesResources.property_setter,
MethodKind.StaticConstructor => FeaturesResources.static_constructor,
MethodKind.Destructor => CSharpFeaturesResources.destructor,
MethodKind.Conversion => CSharpFeaturesResources.conversion_operator,
MethodKind.LocalFunction => FeaturesResources.local_function,
MethodKind.LambdaMethod => CSharpFeaturesResources.lambda,
MethodKind.Ordinary when symbol.Name == WellKnownMemberNames.TopLevelStatementsEntryPointMethodName => CSharpFeaturesResources.top_level_code,
_ => base.GetDisplayName(symbol)
};
protected override string? TryGetDisplayName(SyntaxNode node, EditKind editKind)
=> TryGetDisplayNameImpl(node, editKind);
internal static new string? GetDisplayName(SyntaxNode node, EditKind editKind)
=> TryGetDisplayNameImpl(node, editKind) ?? throw ExceptionUtilities.UnexpectedValue(node.Kind());
internal static string? TryGetDisplayNameImpl(SyntaxNode node, EditKind editKind)
{
switch (node.Kind())
{
// top-level
case SyntaxKind.CompilationUnit:
return CSharpFeaturesResources.top_level_code;
case SyntaxKind.GlobalStatement:
return CSharpFeaturesResources.top_level_statement;
case SyntaxKind.ExternAliasDirective:
return CSharpFeaturesResources.extern_alias;
case SyntaxKind.UsingDirective:
// Dev12 distinguishes using alias from using namespace and reports different errors for removing alias.
// None of these changes are allowed anyways, so let's keep it simple.
return CSharpFeaturesResources.using_directive;
case SyntaxKind.NamespaceDeclaration:
case SyntaxKind.FileScopedNamespaceDeclaration:
return FeaturesResources.namespace_;
case SyntaxKind.ClassDeclaration:
return FeaturesResources.class_;
case SyntaxKind.StructDeclaration:
return FeaturesResources.struct_;
case SyntaxKind.InterfaceDeclaration:
return FeaturesResources.interface_;
case SyntaxKind.RecordDeclaration:
return CSharpFeaturesResources.record_;
case SyntaxKind.RecordStructDeclaration:
return CSharpFeaturesResources.record_struct;
case SyntaxKind.EnumDeclaration:
return FeaturesResources.enum_;
case SyntaxKind.DelegateDeclaration:
return FeaturesResources.delegate_;
case SyntaxKind.FieldDeclaration:
var declaration = (FieldDeclarationSyntax)node;
return declaration.Modifiers.Any(SyntaxKind.ConstKeyword) ? FeaturesResources.const_field : FeaturesResources.field;
case SyntaxKind.EventFieldDeclaration:
return CSharpFeaturesResources.event_field;
case SyntaxKind.VariableDeclaration:
case SyntaxKind.VariableDeclarator:
return TryGetDisplayNameImpl(node.Parent!, editKind);
case SyntaxKind.MethodDeclaration:
return FeaturesResources.method;
case SyntaxKind.ConversionOperatorDeclaration:
return CSharpFeaturesResources.conversion_operator;
case SyntaxKind.OperatorDeclaration:
return FeaturesResources.operator_;
case SyntaxKind.ConstructorDeclaration:
var ctor = (ConstructorDeclarationSyntax)node;
return ctor.Modifiers.Any(SyntaxKind.StaticKeyword) ? FeaturesResources.static_constructor : FeaturesResources.constructor;
case SyntaxKind.DestructorDeclaration:
return CSharpFeaturesResources.destructor;
case SyntaxKind.PropertyDeclaration:
return SyntaxUtilities.HasBackingField((PropertyDeclarationSyntax)node) ? FeaturesResources.auto_property : FeaturesResources.property_;
case SyntaxKind.IndexerDeclaration:
return CSharpFeaturesResources.indexer;
case SyntaxKind.EventDeclaration:
return FeaturesResources.event_;
case SyntaxKind.EnumMemberDeclaration:
return FeaturesResources.enum_value;
case SyntaxKind.GetAccessorDeclaration:
if (node.Parent!.Parent!.IsKind(SyntaxKind.PropertyDeclaration))
{
return CSharpFeaturesResources.property_getter;
}
else
{
RoslynDebug.Assert(node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration));
return CSharpFeaturesResources.indexer_getter;
}
case SyntaxKind.InitAccessorDeclaration:
case SyntaxKind.SetAccessorDeclaration:
if (node.Parent!.Parent!.IsKind(SyntaxKind.PropertyDeclaration))
{
return CSharpFeaturesResources.property_setter;
}
else
{
RoslynDebug.Assert(node.Parent.Parent.IsKind(SyntaxKind.IndexerDeclaration));
return CSharpFeaturesResources.indexer_setter;
}
case SyntaxKind.AddAccessorDeclaration:
case SyntaxKind.RemoveAccessorDeclaration:
return FeaturesResources.event_accessor;
case SyntaxKind.ArrowExpressionClause:
return node.Parent!.Kind() switch
{
SyntaxKind.PropertyDeclaration => CSharpFeaturesResources.property_getter,
SyntaxKind.IndexerDeclaration => CSharpFeaturesResources.indexer_getter,
_ => null
};
case SyntaxKind.TypeParameterConstraintClause:
return FeaturesResources.type_constraint;
case SyntaxKind.TypeParameterList:
case SyntaxKind.TypeParameter:
return FeaturesResources.type_parameter;
case SyntaxKind.Parameter:
return FeaturesResources.parameter;
case SyntaxKind.ParameterList:
return node.Parent is TypeDeclarationSyntax ? FeaturesResources.constructor : null;
case SyntaxKind.AttributeList:
return FeaturesResources.attribute;
case SyntaxKind.Attribute:
return FeaturesResources.attribute;
case SyntaxKind.AttributeTargetSpecifier:
return CSharpFeaturesResources.attribute_target;
// statement:
case SyntaxKind.TryStatement:
return CSharpFeaturesResources.try_block;
case SyntaxKind.CatchClause:
case SyntaxKind.CatchDeclaration:
return CSharpFeaturesResources.catch_clause;
case SyntaxKind.CatchFilterClause:
return CSharpFeaturesResources.filter_clause;
case SyntaxKind.FinallyClause:
return CSharpFeaturesResources.finally_clause;
case SyntaxKind.FixedStatement:
return CSharpFeaturesResources.fixed_statement;
case SyntaxKind.UsingStatement:
return CSharpFeaturesResources.using_statement;
case SyntaxKind.LockStatement:
return CSharpFeaturesResources.lock_statement;
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
return CSharpFeaturesResources.foreach_statement;
case SyntaxKind.CheckedStatement:
return CSharpFeaturesResources.checked_statement;
case SyntaxKind.UncheckedStatement:
return CSharpFeaturesResources.unchecked_statement;
case SyntaxKind.YieldBreakStatement:
return CSharpFeaturesResources.yield_break_statement;
case SyntaxKind.YieldReturnStatement:
return CSharpFeaturesResources.yield_return_statement;
case SyntaxKind.AwaitExpression:
return CSharpFeaturesResources.await_expression;
case SyntaxKind.ParenthesizedLambdaExpression:
case SyntaxKind.SimpleLambdaExpression:
return CSharpFeaturesResources.lambda;
case SyntaxKind.AnonymousMethodExpression:
return CSharpFeaturesResources.anonymous_method;
case SyntaxKind.FromClause:
return CSharpFeaturesResources.from_clause;
case SyntaxKind.JoinClause:
case SyntaxKind.JoinIntoClause:
return CSharpFeaturesResources.join_clause;
case SyntaxKind.LetClause:
return CSharpFeaturesResources.let_clause;
case SyntaxKind.WhereClause:
return CSharpFeaturesResources.where_clause;
case SyntaxKind.OrderByClause:
case SyntaxKind.AscendingOrdering:
case SyntaxKind.DescendingOrdering:
return CSharpFeaturesResources.orderby_clause;
case SyntaxKind.SelectClause:
return CSharpFeaturesResources.select_clause;
case SyntaxKind.GroupClause:
return CSharpFeaturesResources.groupby_clause;
case SyntaxKind.QueryBody:
return CSharpFeaturesResources.query_body;
case SyntaxKind.QueryContinuation:
return CSharpFeaturesResources.into_clause;
case SyntaxKind.IsPatternExpression:
return CSharpFeaturesResources.is_pattern;
case SyntaxKind.SimpleAssignmentExpression:
if (((AssignmentExpressionSyntax)node).IsDeconstruction())
{
return CSharpFeaturesResources.deconstruction;
}
else
{
throw ExceptionUtilities.UnexpectedValue(node.Kind());
}
case SyntaxKind.TupleType:
case SyntaxKind.TupleExpression:
return CSharpFeaturesResources.tuple;
case SyntaxKind.LocalFunctionStatement:
return FeaturesResources.local_function;
case SyntaxKind.DeclarationExpression:
return CSharpFeaturesResources.out_var;
case SyntaxKind.RefType:
case SyntaxKind.RefExpression:
return CSharpFeaturesResources.ref_local_or_expression;
case SyntaxKind.SwitchStatement:
return CSharpFeaturesResources.switch_statement;
case SyntaxKind.LocalDeclarationStatement:
if (((LocalDeclarationStatementSyntax)node).UsingKeyword.IsKind(SyntaxKind.UsingKeyword))
{
return CSharpFeaturesResources.using_declaration;
}
return CSharpFeaturesResources.local_variable_declaration;
default:
return null;
}
}
protected override string GetSuspensionPointDisplayName(SyntaxNode node, EditKind editKind)
{
switch (node.Kind())
{
case SyntaxKind.ForEachStatement:
Debug.Assert(((CommonForEachStatementSyntax)node).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword));
return CSharpFeaturesResources.asynchronous_foreach_statement;
case SyntaxKind.VariableDeclarator:
RoslynDebug.Assert(((LocalDeclarationStatementSyntax)node.Parent!.Parent!).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword));
return CSharpFeaturesResources.asynchronous_using_declaration;
default:
return base.GetSuspensionPointDisplayName(node, editKind);
}
}
#endregion
#region Top-Level Syntactic Rude Edits
private readonly struct EditClassifier
{
private readonly CSharpEditAndContinueAnalyzer _analyzer;
private readonly ArrayBuilder<RudeEditDiagnostic> _diagnostics;
private readonly Match<SyntaxNode>? _match;
private readonly SyntaxNode? _oldNode;
private readonly SyntaxNode? _newNode;
private readonly EditKind _kind;
private readonly TextSpan? _span;
public EditClassifier(
CSharpEditAndContinueAnalyzer analyzer,
ArrayBuilder<RudeEditDiagnostic> diagnostics,
SyntaxNode? oldNode,
SyntaxNode? newNode,
EditKind kind,
Match<SyntaxNode>? match = null,
TextSpan? span = null)
{
RoslynDebug.Assert(oldNode != null || newNode != null);
// if the node is deleted we have map that can be used to closest new ancestor
RoslynDebug.Assert(newNode != null || match != null);
_analyzer = analyzer;
_diagnostics = diagnostics;
_oldNode = oldNode;
_newNode = newNode;
_kind = kind;
_span = span;
_match = match;
}
private void ReportError(RudeEditKind kind, SyntaxNode? spanNode = null, SyntaxNode? displayNode = null)
{
var span = (spanNode != null) ? GetDiagnosticSpan(spanNode, _kind) : GetSpan();
var node = displayNode ?? _newNode ?? _oldNode;
var displayName = GetDisplayName(node!, _kind);
_diagnostics.Add(new RudeEditDiagnostic(kind, span, node, arguments: [displayName]));
}
private TextSpan GetSpan()
{
if (_span.HasValue)
{
return _span.Value;
}
if (_newNode == null)
{
return _analyzer.GetDeletedNodeDiagnosticSpan(_match!.Matches, _oldNode!);
}
return GetDiagnosticSpan(_newNode, _kind);
}
public void ClassifyEdit()
{
switch (_kind)
{
case EditKind.Delete:
ClassifyDelete(_oldNode!);
return;
case EditKind.Update:
ClassifyUpdate(_newNode!);
return;
case EditKind.Move:
ClassifyMove(_newNode!);
return;
case EditKind.Insert:
ClassifyInsert(_newNode!);
return;
case EditKind.Reorder:
ClassifyReorder(_newNode!);
return;
default:
throw ExceptionUtilities.UnexpectedValue(_kind);
}
}
private void ClassifyMove(SyntaxNode newNode)
{
if (SupportsMove(newNode))
{
return;
}
ReportError(RudeEditKind.Move);
}
private void ClassifyReorder(SyntaxNode newNode)
{
if (_newNode.IsKind(SyntaxKind.LocalFunctionStatement))
{
return;
}
switch (newNode.Kind())
{
case SyntaxKind.EnumMemberDeclaration:
// To allow this change we would need to check that values of all fields of the enum
// are preserved, or make sure we can update all method bodies that accessed those that changed.
ReportError(RudeEditKind.Move);
return;
case SyntaxKind.TypeParameter:
ReportError(RudeEditKind.Move);
return;
}
}
private void ClassifyInsert(SyntaxNode node)
{
switch (node.Kind())
{
case SyntaxKind.ExternAliasDirective:
ReportError(RudeEditKind.Insert);
return;
case SyntaxKind.Attribute:
case SyntaxKind.AttributeList:
// To allow inserting of attributes we need to check if the inserted attribute
// is a pseudo-custom attribute that CLR allows us to change, or if it is a compiler well-know attribute
// that affects the generated IL, so we defer those checks until semantic analysis.
// Unless the attribute is a module/assembly attribute
if (node.IsParentKind(SyntaxKind.CompilationUnit) || node.Parent.IsParentKind(SyntaxKind.CompilationUnit))
{
ReportError(RudeEditKind.Insert);
}
return;
}
}
private void ClassifyDelete(SyntaxNode oldNode)
{
switch (oldNode.Kind())
{
case SyntaxKind.ExternAliasDirective:
// To allow removal of declarations we would need to update method bodies that
// were previously binding to them but now are binding to another symbol that was previously hidden.
ReportError(RudeEditKind.Delete);
return;
case SyntaxKind.AttributeList:
case SyntaxKind.Attribute:
// To allow removal of attributes we need to check if the removed attribute
// is a pseudo-custom attribute that CLR does not allow us to change, or if it is a compiler well-know attribute
// that affects the generated IL, so we defer those checks until semantic analysis.
// Unless the attribute is a module/assembly attribute
if (oldNode.IsParentKind(SyntaxKind.CompilationUnit) || oldNode.Parent.IsParentKind(SyntaxKind.CompilationUnit))
{
ReportError(RudeEditKind.Delete);
}
return;
}
}
private void ClassifyUpdate(SyntaxNode newNode)
{
switch (newNode.Kind())
{
case SyntaxKind.ExternAliasDirective:
ReportError(RudeEditKind.Update);
return;
case SyntaxKind.Attribute:
// To allow update of attributes we need to check if the updated attribute
// is a pseudo-custom attribute that CLR allows us to change, or if it is a compiler well-know attribute
// that affects the generated IL, so we defer those checks until semantic analysis.
// Unless the attribute is a module/assembly attribute
if (newNode.IsParentKind(SyntaxKind.CompilationUnit) || newNode.Parent.IsParentKind(SyntaxKind.CompilationUnit))
{
ReportError(RudeEditKind.Update);
}
return;
}
}
}
internal override void ReportTopLevelSyntacticRudeEdits(
ArrayBuilder<RudeEditDiagnostic> diagnostics,
Match<SyntaxNode> match,
Edit<SyntaxNode> edit,
Dictionary<SyntaxNode, EditKind> editMap)
{
if (HasParentEdit(editMap, edit))
{
return;
}
var classifier = new EditClassifier(this, diagnostics, edit.OldNode, edit.NewNode, edit.Kind, match);
classifier.ClassifyEdit();
}
internal override bool HasUnsupportedOperation(IEnumerable<SyntaxNode> nodes, [NotNullWhen(true)] out SyntaxNode? unsupportedNode, out RudeEditKind rudeEdit)
{
// Disallow editing the body even if the change is only in trivia.
// The compiler might emit extra temp local variables, which would change stack layout and cause the CLR to fail.
foreach (var node in nodes)
{
if (node.Kind() is SyntaxKind.StackAllocArrayCreationExpression or SyntaxKind.ImplicitStackAllocArrayCreationExpression)
{
unsupportedNode = node;
rudeEdit = RudeEditKind.StackAllocUpdate;
return true;
}
}
unsupportedNode = null;
rudeEdit = RudeEditKind.None;
return false;
}
#endregion
#region Semantic Rude Edits
internal override void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder<RudeEditDiagnostic> diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType)
{
Debug.Assert(IsMember(newSymbol));
var rudeEditKind = newSymbol switch
{
// Inserting extern member into a new or existing type is not allowed.
{ IsExtern: true }
=> RudeEditKind.InsertExtern,
// All rude edits below only apply when inserting into an existing type (not when the type itself is inserted):
_ when !insertingIntoExistingContainingType => RudeEditKind.None,
// inserting any nested type is allowed
INamedTypeSymbol => RudeEditKind.None,
// Inserting virtual or interface member into an existing type is not allowed.
{ IsVirtual: true } or { IsOverride: true } or { IsAbstract: true }
=> RudeEditKind.InsertVirtual,
// Inserting destructor to an existing type is not allowed.
IMethodSymbol { MethodKind: MethodKind.Destructor }
=> RudeEditKind.Insert,
// Inserting operator to an existing type is not allowed.
IMethodSymbol { MethodKind: MethodKind.Conversion or MethodKind.UserDefinedOperator }
=> RudeEditKind.InsertOperator,
// Inserting a method that explictly implements an interface method into an existing type is not allowed.
IMethodSymbol { ExplicitInterfaceImplementations.IsEmpty: false }
=> RudeEditKind.InsertMethodWithExplicitInterfaceSpecifier,
// TODO: Inserting non-virtual member to an interface (https://github.com/dotnet/roslyn/issues/37128)
{ ContainingType.TypeKind: TypeKind.Interface }
=> RudeEditKind.InsertIntoInterface,
// Inserting a field into an enum:
#pragma warning disable format // https://github.com/dotnet/roslyn/issues/54759
IFieldSymbol { ContainingType.TypeKind: TypeKind.Enum }
=> RudeEditKind.Insert,
#pragma warning restore format
_ => RudeEditKind.None
};
if (rudeEditKind != RudeEditKind.None)
{
diagnostics.Add(new RudeEditDiagnostic(
rudeEditKind,
GetDiagnosticSpan(newNode, EditKind.Insert),
newNode,
arguments: [GetDisplayName(newNode, EditKind.Insert)]));
}
}
#endregion
#region Exception Handling Rude Edits
/// <summary>
/// Return nodes that represent exception handlers encompassing the given active statement node.
/// </summary>
protected override List<SyntaxNode> GetExceptionHandlingAncestors(SyntaxNode node, SyntaxNode root, bool isNonLeaf)
{
var result = new List<SyntaxNode>();
var current = node;
while (current != root)
{
var kind = current.Kind();
switch (kind)
{
case SyntaxKind.TryStatement:
if (isNonLeaf)
{
result.Add(current);
}
break;
case SyntaxKind.CatchClause:
case SyntaxKind.FinallyClause:
result.Add(current);
// skip try:
RoslynDebug.Assert(current.Parent is object);
RoslynDebug.Assert(current.Parent.Kind() == SyntaxKind.TryStatement);
current = current.Parent;
break;
// stop at type declaration:
case SyntaxKind.ClassDeclaration:
case SyntaxKind.StructDeclaration:
case SyntaxKind.RecordDeclaration:
case SyntaxKind.RecordStructDeclaration:
return result;
}
// stop at lambda:
if (LambdaUtilities.IsLambda(current))
{
return result;
}
Debug.Assert(current.Parent != null);
current = current.Parent;
}
return result;
}
internal override void ReportEnclosingExceptionHandlingRudeEdits(
ArrayBuilder<RudeEditDiagnostic> diagnostics,
IEnumerable<Edit<SyntaxNode>> exceptionHandlingEdits,
SyntaxNode oldStatement,
TextSpan newStatementSpan)
{
foreach (var edit in exceptionHandlingEdits)
{
// try/catch/finally have distinct labels so only the nodes of the same kind may match:
Debug.Assert(edit.Kind != EditKind.Update || edit.OldNode.RawKind == edit.NewNode.RawKind);
if (edit.Kind != EditKind.Update || !AreExceptionClausesEquivalent(edit.OldNode, edit.NewNode))
{
AddAroundActiveStatementRudeDiagnostic(diagnostics, edit.OldNode, edit.NewNode, newStatementSpan);
}
}
}
private static bool AreExceptionClausesEquivalent(SyntaxNode oldNode, SyntaxNode newNode)
{
switch (oldNode.Kind())
{
case SyntaxKind.TryStatement:
var oldTryStatement = (TryStatementSyntax)oldNode;
var newTryStatement = (TryStatementSyntax)newNode;
return SyntaxFactory.AreEquivalent(oldTryStatement.Finally, newTryStatement.Finally)
&& SyntaxFactory.AreEquivalent(oldTryStatement.Catches, newTryStatement.Catches);
case SyntaxKind.CatchClause:
case SyntaxKind.FinallyClause:
return SyntaxFactory.AreEquivalent(oldNode, newNode);
default:
throw ExceptionUtilities.UnexpectedValue(oldNode.Kind());
}
}
/// <summary>
/// An active statement (leaf or not) inside a "catch" makes the catch block read-only.
/// An active statement (leaf or not) inside a "finally" makes the whole try/catch/finally block read-only.
/// An active statement (non leaf) inside a "try" makes the catch/finally block read-only.
/// </summary>
/// <remarks>
/// Exception handling regions are only needed to be tracked if they contain user code.
/// <see cref="UsingStatementSyntax"/> and using <see cref="LocalDeclarationStatementSyntax"/> generate finally blocks,
/// but they do not contain non-hidden sequence points.
/// </remarks>
/// <param name="node">An exception handling ancestor of an active statement node.</param>
/// <param name="coversAllChildren">
/// True if all child nodes of the <paramref name="node"/> are contained in the exception region represented by the <paramref name="node"/>.
/// </param>
protected override TextSpan GetExceptionHandlingRegion(SyntaxNode node, out bool coversAllChildren)
{
TryStatementSyntax tryStatement;
switch (node.Kind())
{
case SyntaxKind.TryStatement:
tryStatement = (TryStatementSyntax)node;
coversAllChildren = false;
if (tryStatement.Catches.Count == 0)
{
RoslynDebug.Assert(tryStatement.Finally != null);
return tryStatement.Finally.Span;
}
return TextSpan.FromBounds(
tryStatement.Catches.First().SpanStart,
(tryStatement.Finally != null)
? tryStatement.Finally.Span.End
: tryStatement.Catches.Last().Span.End);
case SyntaxKind.CatchClause:
coversAllChildren = true;
return node.Span;
case SyntaxKind.FinallyClause:
coversAllChildren = true;
tryStatement = (TryStatementSyntax)node.Parent!;
return tryStatement.Span;
default:
throw ExceptionUtilities.UnexpectedValue(node.Kind());
}
}
#endregion
#region State Machines
internal override bool IsStateMachineMethod(SyntaxNode declaration)
=> SyntaxUtilities.IsAsyncDeclaration(declaration) || SyntaxUtilities.IsIterator(declaration);
internal override void ReportStateMachineSuspensionPointRudeEdits(DiagnosticContext diagnosticContext, SyntaxNode oldNode, SyntaxNode newNode)
{
if (newNode.IsKind(SyntaxKind.AwaitExpression) && oldNode.IsKind(SyntaxKind.AwaitExpression))
{
var oldContainingStatementPart = FindContainingStatementPart(oldNode);
var newContainingStatementPart = FindContainingStatementPart(newNode);
// If the old statement has spilled state and the new doesn't the edit is ok. We'll just not use the spilled state.
if (!SyntaxFactory.AreEquivalent(oldContainingStatementPart, newContainingStatementPart) &&
!HasNoSpilledState(newNode, newContainingStatementPart))
{
diagnosticContext.Report(RudeEditKind.AwaitStatementUpdate, newContainingStatementPart.Span);
}
}
}
private static SyntaxNode FindContainingStatementPart(SyntaxNode node)
{
while (true)
{
if (node is StatementSyntax statement)
{
return statement;
}
RoslynDebug.Assert(node is object);
RoslynDebug.Assert(node.Parent is object);
switch (node.Parent.Kind())
{
case SyntaxKind.ForStatement:
case SyntaxKind.ForEachStatement:
case SyntaxKind.IfStatement:
case SyntaxKind.WhileStatement:
case SyntaxKind.DoStatement:
case SyntaxKind.SwitchStatement:
case SyntaxKind.LockStatement:
case SyntaxKind.UsingStatement:
case SyntaxKind.ArrowExpressionClause:
return node;
}
if (LambdaUtilities.IsLambdaBodyStatementOrExpression(node))
{
return node;
}
node = node.Parent;
}
}
private static bool HasNoSpilledState(SyntaxNode awaitExpression, SyntaxNode containingStatementPart)
{
Debug.Assert(awaitExpression.IsKind(SyntaxKind.AwaitExpression));
// There is nothing within the statement part surrounding the await expression.
if (containingStatementPart == awaitExpression)
{
return true;
}
switch (containingStatementPart.Kind())
{
case SyntaxKind.ExpressionStatement:
case SyntaxKind.ReturnStatement:
var expression = GetExpressionFromStatementPart(containingStatementPart);
// await expr;
// return await expr;
if (expression == awaitExpression)
{
return true;
}
// identifier = await expr;
// return identifier = await expr;
return IsSimpleAwaitAssignment(expression, awaitExpression);
case SyntaxKind.VariableDeclaration:
// var idf = await expr in using, for, etc.
// EqualsValueClause -> VariableDeclarator -> VariableDeclaration
return awaitExpression.Parent!.Parent!.Parent == containingStatementPart;
case SyntaxKind.LocalDeclarationStatement:
// var idf = await expr;
// EqualsValueClause -> VariableDeclarator -> VariableDeclaration -> LocalDeclarationStatement
return awaitExpression.Parent!.Parent!.Parent!.Parent == containingStatementPart;
}
return IsSimpleAwaitAssignment(containingStatementPart, awaitExpression);
}
private static ExpressionSyntax GetExpressionFromStatementPart(SyntaxNode statement)
{
switch (statement.Kind())
{
case SyntaxKind.ExpressionStatement:
return ((ExpressionStatementSyntax)statement).Expression;
case SyntaxKind.ReturnStatement:
// Must have an expression since we are only inspecting at statements that contain an expression.
return ((ReturnStatementSyntax)statement).Expression!;
default:
throw ExceptionUtilities.UnexpectedValue(statement.Kind());
}
}
private static bool IsSimpleAwaitAssignment(SyntaxNode node, SyntaxNode awaitExpression)
{
if (node is AssignmentExpressionSyntax(SyntaxKind.SimpleAssignmentExpression) assignment)
{
return assignment.Left.IsKind(SyntaxKind.IdentifierName) && assignment.Right == awaitExpression;
}
return false;
}
#endregion
#region Rude Edits around Active Statement
internal override void ReportOtherRudeEditsAroundActiveStatement(
ArrayBuilder<RudeEditDiagnostic> diagnostics,
IReadOnlyDictionary<SyntaxNode, SyntaxNode> reverseMap,
SyntaxNode oldActiveStatement,
DeclarationBody oldBody,
SemanticModel oldModel,
SyntaxNode newActiveStatement,
DeclarationBody newBody,
SemanticModel newModel,
bool isNonLeaf,
CancellationToken cancellationToken)
{
ReportRudeEditsForSwitchWhenClauses(diagnostics, oldActiveStatement, newActiveStatement);
ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, reverseMap, oldActiveStatement, oldBody.EncompassingAncestor, oldModel, newActiveStatement, newBody.EncompassingAncestor, newModel, cancellationToken);
ReportRudeEditsForCheckedStatements(diagnostics, oldActiveStatement, newActiveStatement, isNonLeaf);
}
/// <summary>
/// Reports rude edits when an active statement is a when clause in a switch statement and any of the switch cases or the switch value changed.
/// This is necessary since the switch emits long-lived synthesized variables to store results of pattern evaluations.
/// These synthesized variables are mapped to the slots of the new methods via ordinals. The mapping preserves the values of these variables as long as
/// exactly the same variables are emitted for the new switch as they were for the old one and their order didn't change either.
/// This is guaranteed if none of the case clauses have changed.
/// </summary>
private void ReportRudeEditsForSwitchWhenClauses(ArrayBuilder<RudeEditDiagnostic> diagnostics, SyntaxNode oldActiveStatement, SyntaxNode newActiveStatement)
{
if (!oldActiveStatement.IsKind(SyntaxKind.WhenClause))
{
return;
}
// switch expression does not have sequence points (active statements):
if (oldActiveStatement.Parent!.Parent!.Parent is not SwitchStatementSyntax oldSwitch)
{
return;
}
// switch statement does not match switch expression, so it must be part of a switch statement as well.
var newSwitch = (SwitchStatementSyntax)newActiveStatement.Parent!.Parent!.Parent!;
// when clauses can only match other when clauses:
Debug.Assert(newActiveStatement.IsKind(SyntaxKind.WhenClause));
if (!AreEquivalentIgnoringLambdaBodies(oldSwitch.Expression, newSwitch.Expression))
{
AddRudeUpdateAroundActiveStatement(diagnostics, newSwitch);
}
if (!AreEquivalentSwitchStatementDecisionTrees(oldSwitch, newSwitch))
{
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.UpdateAroundActiveStatement,
GetDiagnosticSpan(newSwitch, EditKind.Update),
newSwitch,
[CSharpFeaturesResources.switch_statement_case_clause]));
}
}
private static bool AreEquivalentSwitchStatementDecisionTrees(SwitchStatementSyntax oldSwitch, SwitchStatementSyntax newSwitch)
=> oldSwitch.Sections.SequenceEqual(newSwitch.Sections, AreSwitchSectionsEquivalent);
private static bool AreSwitchSectionsEquivalent(SwitchSectionSyntax oldSection, SwitchSectionSyntax newSection)
=> oldSection.Labels.SequenceEqual(newSection.Labels, AreLabelsEquivalent);
private static bool AreLabelsEquivalent(SwitchLabelSyntax oldLabel, SwitchLabelSyntax newLabel)
{
if (oldLabel is CasePatternSwitchLabelSyntax oldCasePatternLabel &&
newLabel is CasePatternSwitchLabelSyntax newCasePatternLabel)
{
// ignore the actual when expressions:
return SyntaxFactory.AreEquivalent(oldCasePatternLabel.Pattern, newCasePatternLabel.Pattern) &&
(oldCasePatternLabel.WhenClause != null) == (newCasePatternLabel.WhenClause != null);
}
else
{
return SyntaxFactory.AreEquivalent(oldLabel, newLabel);
}
}
private void ReportRudeEditsForCheckedStatements(
ArrayBuilder<RudeEditDiagnostic> diagnostics,
SyntaxNode oldActiveStatement,
SyntaxNode newActiveStatement,
bool isNonLeaf)
{
// checked context can't be changed around non-leaf active statement:
if (!isNonLeaf)
{
return;
}
// Changing checked context around an internal active statement may change the instructions
// executed after method calls in the active statement but before the next sequence point.
// Since the debugger remaps the IP at the first sequence point following a call instruction
// allowing overflow context to be changed may lead to execution of code with old semantics.
var oldCheckedStatement = TryGetCheckedStatementAncestor(oldActiveStatement);
var newCheckedStatement = TryGetCheckedStatementAncestor(newActiveStatement);
bool isRude;
if (oldCheckedStatement == null || newCheckedStatement == null)
{
isRude = oldCheckedStatement != newCheckedStatement;
}
else
{
isRude = oldCheckedStatement.Kind() != newCheckedStatement.Kind();
}
if (isRude)
{
AddAroundActiveStatementRudeDiagnostic(diagnostics, oldCheckedStatement, newCheckedStatement, newActiveStatement.Span);
}
}
private static CheckedStatementSyntax? TryGetCheckedStatementAncestor(SyntaxNode? node)
{
// Ignoring lambda boundaries since checked context flows through.
while (node != null)
{
switch (node.Kind())
{
case SyntaxKind.CheckedStatement:
case SyntaxKind.UncheckedStatement:
return (CheckedStatementSyntax)node;
}
node = node.Parent;
}
return null;
}
private void ReportRudeEditsForAncestorsDeclaringInterStatementTemps(
ArrayBuilder<RudeEditDiagnostic> diagnostics,
IReadOnlyDictionary<SyntaxNode, SyntaxNode> reverseMap,
SyntaxNode oldActiveStatement,
SyntaxNode oldEncompassingAncestor,
SemanticModel oldModel,
SyntaxNode newActiveStatement,
SyntaxNode newEncompassingAncestor,
SemanticModel newModel,
CancellationToken cancellationToken)
{
// Rude Edits for fixed/using/lock/foreach statements that are added/updated around an active statement.
// Although such changes are technically possible, they might lead to confusion since
// the temporary variables these statements generate won't be properly initialized.
//
// We use a simple algorithm to match each new node with its old counterpart.
// If all nodes match this algorithm is linear, otherwise it's quadratic.
//
// Unlike exception regions matching where we use LCS, we allow reordering of the statements.
ReportUnmatchedStatements<LockStatementSyntax>(
diagnostics,
reverseMap,
oldActiveStatement,
oldEncompassingAncestor,
oldModel,
newActiveStatement,
newEncompassingAncestor,
newModel,
nodeSelector: static n => n.IsKind(SyntaxKind.LockStatement),
getTypedNodes: static n => OneOrMany.OneOrNone<SyntaxNode>(n.Expression),
areEquivalent: AreEquivalentActiveStatements,
areSimilar: null,
cancellationToken: cancellationToken);
ReportUnmatchedStatements<FixedStatementSyntax>(
diagnostics,
reverseMap,
oldActiveStatement,
oldEncompassingAncestor,
oldModel,
newActiveStatement,
newEncompassingAncestor,
newModel,
nodeSelector: static n => n.IsKind(SyntaxKind.FixedStatement),
getTypedNodes: static n => GetTypedNodes(n.Declaration),
areEquivalent: AreEquivalentActiveStatements,
areSimilar: static (n1, n2) => DeclareSameIdentifiers(n1.Declaration.Variables, n2.Declaration.Variables),
cancellationToken: cancellationToken);
// Using statements with declaration do not introduce compiler generated temporary.
ReportUnmatchedStatements<UsingStatementSyntax>(
diagnostics,
reverseMap,
oldActiveStatement,
oldEncompassingAncestor,
oldModel,
newActiveStatement,
newEncompassingAncestor,
newModel,
nodeSelector: static n => n is UsingStatementSyntax { Declaration: null } usingStatement,
getTypedNodes: static n => OneOrMany.Create<SyntaxNode>(n.Expression!),
areEquivalent: AreEquivalentActiveStatements,
areSimilar: null,
cancellationToken: cancellationToken);
ReportUnmatchedStatements<CommonForEachStatementSyntax>(
diagnostics,
reverseMap,
oldActiveStatement,
oldEncompassingAncestor,
oldModel,
newActiveStatement,
newEncompassingAncestor,
newModel,
nodeSelector: static n => n.Kind() is SyntaxKind.ForEachStatement or SyntaxKind.ForEachVariableStatement,
getTypedNodes: static n => OneOrMany.OneOrNone<SyntaxNode>(n.Expression),
areEquivalent: AreEquivalentActiveStatements,
areSimilar: AreSimilarActiveStatements,
cancellationToken: cancellationToken);
static OneOrMany<SyntaxNode> GetTypedNodes(VariableDeclarationSyntax declaration)
=> (declaration.Variables is [{ Initializer: { } initializer }])
? OneOrMany.Create<SyntaxNode>(initializer.Value)
: OneOrMany.Create(declaration.Variables.Select(static v => (SyntaxNode?)v.Initializer?.Value).WhereNotNull().ToImmutableArray());
}
private static bool DeclareSameIdentifiers(SeparatedSyntaxList<VariableDeclaratorSyntax> oldVariables, SeparatedSyntaxList<VariableDeclaratorSyntax> newVariables)
=> DeclareSameIdentifiers(oldVariables.Select(v => v.Identifier).ToArray(), newVariables.Select(v => v.Identifier).ToArray());
private static bool DeclareSameIdentifiers(SyntaxToken[] oldVariables, SyntaxToken[] newVariables)
{
if (oldVariables.Length != newVariables.Length)
{
return false;
}
for (var i = 0; i < oldVariables.Length; i++)
{
if (!SyntaxFactory.AreEquivalent(oldVariables[i], newVariables[i]))
{
return false;
}
}
return true;
}
#endregion
}
|