|
// 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.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue
{
internal sealed class CSharpEditAndContinueAnalyzer : AbstractEditAndContinueAnalyzer
{
[ExportLanguageServiceFactory(typeof(IEditAndContinueAnalyzer), LanguageNames.CSharp), Shared]
internal sealed class Factory : ILanguageServiceFactory
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public Factory()
{
}
public ILanguageService CreateLanguageService(HostLanguageServices languageServices)
{
return new CSharpEditAndContinueAnalyzer(testFaultInjector: null);
}
}
// Public for testing purposes
public CSharpEditAndContinueAnalyzer(Action<SyntaxNode>? testFaultInjector = null)
: base(testFaultInjector)
{
}
#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.
/// </returns>
internal override bool TryFindMemberDeclaration(SyntaxNode? root, SyntaxNode node, out OneOrMany<SyntaxNode> declarations)
{
var current = node;
while (current != null && current != root)
{
switch (current.Kind())
{
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|];
RoslynDebug.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.VariableDeclarator:
// public static int F = 1, [|G = 2|];
RoslynDebug.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;
}
current = current.Parent;
}
declarations = default;
return false;
}
/// <returns>
/// Given a node representing a declaration or a top-level match node returns:
/// - <see cref="BlockSyntax"/> for method-like member declarations with block bodies (methods, operators, constructors, destructors, accessors).
/// - <see cref="ExpressionSyntax"/> for variable declarators of fields, properties with an initializer expression, or
/// for method-like member declarations with expression bodies (methods, properties, indexers, operators)
/// - <see cref="CompilationUnitSyntax"/> for top level statements
///
/// A null reference otherwise.
/// </returns>
internal override SyntaxNode? TryGetDeclarationBody(SyntaxNode node)
{
if (node is VariableDeclaratorSyntax variableDeclarator)
{
return variableDeclarator.Initializer?.Value;
}
if (IsCompilationUnitWithGlobalStatements(node))
{
// For top level statements, where there is no syntax node to represent the entire body of the synthesized
// main method we just use the compilation unit itself
return node;
}
return SyntaxUtilities.TryGetMethodDeclarationBody(node);
}
internal override bool IsDeclarationWithSharedBody(SyntaxNode declaration)
=> false;
protected override ImmutableArray<ISymbol> GetCapturedVariables(SemanticModel model, SyntaxNode memberBody)
{
if (memberBody is CompilationUnitSyntax unit && unit.ContainsGlobalStatements())
{
return model.AnalyzeDataFlow(((GlobalStatementSyntax)unit.Members[0]).Statement, unit.Members.OfType<GlobalStatementSyntax>().Last().Statement)!.Captured;
}
Debug.Assert(memberBody.IsKind(SyntaxKind.Block) || memberBody is ExpressionSyntax);
return model.AnalyzeDataFlow(memberBody).Captured;
}
protected override bool AreHandledEventsEqual(IMethodSymbol oldMethod, IMethodSymbol newMethod)
=> true;
internal override bool HasParameterClosureScope(ISymbol member)
{
// in instance constructor parameters are lifted to a closure different from method body
return (member as IMethodSymbol)?.MethodKind == MethodKind.Constructor;
}
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;
}
/// <returns>
/// If <paramref name="node"/> is a method, accessor, operator, destructor, or constructor without an initializer,
/// tokens of its block body, or tokens of the expression body.
///
/// If <paramref name="node"/> is an indexer declaration the tokens of its expression body.
///
/// If <paramref name="node"/> is a property declaration the tokens of its expression body or initializer.
///
/// If <paramref name="node"/> is a constructor with an initializer,
/// tokens of the initializer concatenated with tokens of the constructor body.
///
/// If <paramref name="node"/> is a variable declarator of a field with an initializer,
/// subset of the tokens of the field declaration depending on which variable declarator it is.
///
/// If <paramref name="node"/> is a <see cref="CompilationUnitSyntax"/> the tokens of all its global statements.
/// Null reference otherwise.
/// </returns>
internal override IEnumerable<SyntaxToken>? TryGetActiveTokens(SyntaxNode node)
{
if (node.IsKind(SyntaxKind.VariableDeclarator))
{
// TODO: The logic is similar to BreakpointSpans.TryCreateSpanForVariableDeclaration. Can we abstract it?
var declarator = node;
var fieldDeclaration = (BaseFieldDeclarationSyntax)declarator.Parent!.Parent!;
var variableDeclaration = fieldDeclaration.Declaration;
if (fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword))
{
return null;
}
if (variableDeclaration.Variables.Count == 1)
{
if (variableDeclaration.Variables[0].Initializer == null)
{
return null;
}
return fieldDeclaration.Modifiers.Concat(variableDeclaration.DescendantTokens()).Concat(fieldDeclaration.SemicolonToken);
}
if (declarator == variableDeclaration.Variables[0])
{
return fieldDeclaration.Modifiers.Concat(variableDeclaration.Type.DescendantTokens()).Concat(node.DescendantTokens());
}
return declarator.DescendantTokens();
}
if (node is PropertyDeclarationSyntax { ExpressionBody: var propertyExpressionBody and not null })
{
return propertyExpressionBody.Expression.DescendantTokens();
}
if (node is IndexerDeclarationSyntax { ExpressionBody: var indexerExpressionBody and not null })
{
return indexerExpressionBody.Expression.DescendantTokens();
}
if (node is CompilationUnitSyntax unit && unit.ContainsGlobalStatements())
{
return unit.Members.OfType<GlobalStatementSyntax>().SelectMany(globalStatement => globalStatement.DescendantTokens());
}
var bodyTokens = SyntaxUtilities.TryGetMethodDeclarationBody(node)?.DescendantTokens();
if (node is ConstructorDeclarationSyntax ctor)
{
if (ctor.Initializer != null)
{
bodyTokens = ctor.Initializer.DescendantTokens().Concat(bodyTokens ?? Enumerable.Empty<SyntaxToken>());
}
}
return bodyTokens;
}
internal override (TextSpan envelope, TextSpan hole) GetActiveSpanEnvelope(SyntaxNode declaration)
=> (BreakpointSpans.GetEnvelope(declaration), default);
protected override SyntaxNode GetEncompassingAncestorImpl(SyntaxNode bodyOrMatchRoot)
{
// Constructor may contain active nodes outside of its body (constructor initializer),
// but within the body of the member declaration (the parent).
if (bodyOrMatchRoot.Parent.IsKind(SyntaxKind.ConstructorDeclaration))
{
return bodyOrMatchRoot.Parent;
}
// Field initializer match root -- an active statement may include the modifiers
// and type specification of the field declaration.
if (bodyOrMatchRoot.IsKind(SyntaxKind.EqualsValueClause) &&
bodyOrMatchRoot.Parent.IsKind(SyntaxKind.VariableDeclarator) &&
bodyOrMatchRoot.Parent.Parent.IsKind(SyntaxKind.FieldDeclaration))
{
return bodyOrMatchRoot.Parent.Parent;
}
// Field initializer body -- an active statement may include the modifiers
// and type specification of the field declaration.
if (bodyOrMatchRoot.Parent.IsKind(SyntaxKind.EqualsValueClause) &&
bodyOrMatchRoot.Parent.Parent.IsKind(SyntaxKind.VariableDeclarator) &&
bodyOrMatchRoot.Parent.Parent.Parent.IsKind(SyntaxKind.FieldDeclaration))
{
return bodyOrMatchRoot.Parent.Parent.Parent;
}
// otherwise all active statements are covered by the body/match root itself:
return bodyOrMatchRoot;
}
protected override SyntaxNode FindStatementAndPartner(SyntaxNode declarationBody, TextSpan span, SyntaxNode? partnerDeclarationBody, out SyntaxNode? partner, out int statementPart)
{
var position = span.Start;
SyntaxUtilities.AssertIsBody(declarationBody, allowLambda: false);
if (position < declarationBody.SpanStart)
{
// Only constructors and the field initializers may have an [|active statement|] starting outside of the <<body>>.
// Constructor: [|public C()|] <<{ }>>
// Constructor initializer: public C() : [|base(expr)|] <<{ }>>
// Constructor initializer with lambda: public C() : base(() => { [|...|] }) <<{ }>>
// Field initializers: [|public int a = <<expr>>|], [|b = <<expr>>|];
// No need to special case property initializers here, the active statement always spans the initializer expression.
if (declarationBody.Parent.IsKind(SyntaxKind.ConstructorDeclaration))
{
var constructor = (ConstructorDeclarationSyntax)declarationBody.Parent;
var partnerConstructor = (ConstructorDeclarationSyntax?)partnerDeclarationBody?.Parent;
if (constructor.Initializer == null || position < constructor.Initializer.ColonToken.SpanStart)
{
statementPart = DefaultStatementPart;
partner = partnerConstructor;
return constructor;
}
declarationBody = constructor.Initializer;
partnerDeclarationBody = partnerConstructor?.Initializer;
}
}
if (!declarationBody.FullSpan.Contains(position))
{
// invalid position, let's find a labeled node that encompasses the body:
position = declarationBody.SpanStart;
}
SyntaxNode node;
if (partnerDeclarationBody != null)
{
SyntaxUtilities.FindLeafNodeAndPartner(declarationBody, position, partnerDeclarationBody, out node, out partner);
}
else
{
node = declarationBody.FindToken(position).Parent!;
partner = null;
}
while (true)
{
var isBody = node == declarationBody || 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 (partner != null)
{
partner = ((VariableDeclarationSyntax)partner).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 (partner != null)
{
partner = partner.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),
};
protected override bool AreEquivalent(SyntaxNode left, SyntaxNode right)
=> SyntaxFactory.AreEquivalent(left, right);
private static bool AreEquivalentIgnoringLambdaBodies(SyntaxNode left, SyntaxNode right)
{
// usual case:
if (SyntaxFactory.AreEquivalent(left, right))
{
return true;
}
return LambdaUtilities.AreEquivalentIgnoringLambdaBodies(left, right);
}
internal override SyntaxNode FindDeclarationBodyPartner(SyntaxNode leftRoot, SyntaxNode rightRoot, SyntaxNode leftNode)
=> SyntaxUtilities.FindPartner(leftRoot, rightRoot, leftNode);
internal override bool IsClosureScope(SyntaxNode node)
=> LambdaUtilities.IsClosureScope(node);
protected override SyntaxNode? FindEnclosingLambdaBody(SyntaxNode? container, SyntaxNode node)
{
var root = GetEncompassingAncestor(container);
var current = node;
while (current != root && current != null)
{
if (LambdaUtilities.IsLambdaBodyStatementOrExpression(current, out var body))
{
return body;
}
current = current.Parent;
}
return null;
}
protected override IEnumerable<SyntaxNode> GetLambdaBodyExpressionsAndStatements(SyntaxNode lambdaBody)
=> SpecializedCollections.SingletonEnumerable(lambdaBody);
protected override SyntaxNode? TryGetPartnerLambdaBody(SyntaxNode oldBody, SyntaxNode newLambda)
=> LambdaUtilities.TryGetCorrespondingLambdaBody(oldBody, newLambda);
protected override Match<SyntaxNode> ComputeTopLevelMatch(SyntaxNode oldCompilationUnit, SyntaxNode newCompilationUnit)
=> SyntaxComparer.TopLevel.ComputeMatch(oldCompilationUnit, newCompilationUnit);
protected override Match<SyntaxNode> ComputeTopLevelDeclarationMatch(SyntaxNode oldDeclaration, SyntaxNode newDeclaration)
{
Contract.ThrowIfNull(oldDeclaration.Parent);
Contract.ThrowIfNull(newDeclaration.Parent);
var comparer = new SyntaxComparer(oldDeclaration.Parent, newDeclaration.Parent, new[] { oldDeclaration }, new[] { newDeclaration }, compareStatementSyntax: false);
return comparer.ComputeMatch(oldDeclaration.Parent, newDeclaration.Parent);
}
protected override 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);
return new SyntaxComparer(oldRoot, newRoot, GetChildNodes(oldRoot, oldBody), GetChildNodes(newRoot, newBody), compareStatementSyntax: true).ComputeMatch(oldRoot, newRoot, knownMatches);
}
if (oldBody.Parent.IsKind(SyntaxKind.ConstructorDeclaration))
{
// We need to include constructor initializer in the match, since it may contain lambdas.
// Use the constructor declaration as a root.
RoslynDebug.Assert(oldBody.IsKind(SyntaxKind.Block));
RoslynDebug.Assert(newBody.IsKind(SyntaxKind.Block));
RoslynDebug.Assert(newBody.Parent.IsKind(SyntaxKind.ConstructorDeclaration));
RoslynDebug.Assert(newBody.Parent is object);
return SyntaxComparer.Statement.ComputeMatch(oldBody.Parent, newBody.Parent, 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 override void ReportDeclarationInsertDeleteRudeEdits(ArrayBuilder<RudeEditDiagnostic> diagnostics, SyntaxNode oldNode, SyntaxNode newNode, ISymbol oldSymbol, ISymbol newSymbol, EditAndContinueCapabilitiesGrantor capabilities, CancellationToken cancellationToken)
{
// Global statements have a declaring syntax reference to the compilation unit itself, which we can just ignore
// for the purposes of declaration rude edits
if (oldNode.IsKind(SyntaxKind.CompilationUnit) || newNode.IsKind(SyntaxKind.CompilationUnit))
{
return;
}
// Compiler generated methods of records have a declaring syntax reference to the record declaration itself
// but their explicitly implemented counterparts reference the actual member. Compiler generated properties
// of records reference the parameter that names them.
//
// Since there is no useful "old" syntax node for these members, we can't compute declaration or body edits
// using the standard tree comparison code.
//
// Based on this, we can detect a new explicit implementation of a record member by checking if the
// declaration kind has changed. If it hasn't changed, then our standard code will handle it.
if (oldNode.RawKind == newNode.RawKind)
{
base.ReportDeclarationInsertDeleteRudeEdits(diagnostics, oldNode, newNode, oldSymbol, newSymbol, capabilities, cancellationToken);
return;
}
// When explicitly implementing a property that is represented by a positional parameter
// what looks like an edit could actually be a rude delete, or something else
if (oldNode is ParameterSyntax &&
newNode is PropertyDeclarationSyntax property)
{
if (property.AccessorList!.Accessors.Count == 1)
{
// Explicitly implementing a property with only one accessor is a delete of the init accessor, so a rude edit.
// Not implementing the get accessor would be a compile error
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.ImplementRecordParameterAsReadOnly,
GetDiagnosticSpan(newNode, EditKind.Delete),
oldNode,
new[] { property.Identifier.ToString() }));
}
else if (property.AccessorList.Accessors.Any(a => a.IsKind(SyntaxKind.SetAccessorDeclaration)))
{
// The compiler implements the properties with an init accessor so explicitly implementing
// it with a set accessor is a rude accessor change edit
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.ImplementRecordParameterWithSet,
GetDiagnosticSpan(newNode, EditKind.Delete),
oldNode,
new[] { property.Identifier.ToString() }));
}
}
else if (oldNode is RecordDeclarationSyntax &&
newNode is MethodDeclarationSyntax &&
!oldSymbol.GetParameters().Select(p => p.Name).SequenceEqual(newSymbol.GetParameters().Select(p => p.Name)) &&
!capabilities.Grant(EditAndContinueCapabilities.UpdateParameters))
{
// Explicitly implemented methods must have parameter names that match the compiler generated versions
// exactly if the runtime doesn't support updating parameters, otherwise the debugger would show incorrect
// parameter names.
// We don't need to worry about parameter types, because if they were different then we wouldn't get here
// as this wouldn't be the explicit implementation of a known method.
// We don't need to worry about access modifiers because the symbol matching still works, and most of the
// time changing access modifiers for these known methods is a compile error anyway.
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.ExplicitRecordMethodParameterNamesMustMatch,
GetDiagnosticSpan(newNode, EditKind.Update),
oldNode,
new[] { oldSymbol.ToDisplayString(SymbolDisplayFormats.NameFormat) }));
}
}
protected override bool TryMatchActiveStatement(
SyntaxNode oldStatement,
int statementPart,
SyntaxNode oldBody,
SyntaxNode newBody,
[NotNullWhen(true)] out SyntaxNode? newStatement)
{
SyntaxUtilities.AssertIsBody(oldBody, allowLambda: true);
SyntaxUtilities.AssertIsBody(newBody, allowLambda: true);
switch (oldStatement.Kind())
{
case SyntaxKind.ThisConstructorInitializer:
case SyntaxKind.BaseConstructorInitializer:
case SyntaxKind.ConstructorDeclaration:
var newConstructor = (ConstructorDeclarationSyntax)(newBody.Parent.IsKind(SyntaxKind.ArrowExpressionClause) ? newBody.Parent.Parent : newBody.Parent)!;
newStatement = (SyntaxNode?)newConstructor.Initializer ?? newConstructor;
return true;
default:
// 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 TextSpan GetGlobalStatementDiagnosticSpan(SyntaxNode node)
{
if (node is CompilationUnitSyntax unit)
{
// When deleting something from a compilation unit we just report diagnostics for the last global statement
return unit.Members.OfType<GlobalStatementSyntax>().LastOrDefault()?.Span ?? default;
}
return GetDiagnosticSpan(node, EditKind.Delete);
}
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(SyntaxNode root, int position, out TextSpan span)
=> BreakpointSpans.TryGetClosestBreakpointSpan(root, position, out span);
protected override bool TryGetActiveSpan(SyntaxNode node, int statementPart, int minLength, out TextSpan span)
{
switch (node.Kind())
{
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, 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, 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;
default:
// make sure all nodes that use statement parts are handled above:
Debug.Assert(statementPart == DefaultStatementPart);
return BreakpointSpans.TryGetClosestBreakpointSpan(node, node.SpanStart, out span);
}
}
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.ToArray(), newTokens.ToArray());
}
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 HasBackingField(SyntaxNode propertyOrIndexerDeclaration)
=> propertyOrIndexerDeclaration is PropertyDeclarationSyntax propertyDecl &&
SyntaxUtilities.HasBackingField(propertyDecl);
internal override bool TryGetAssociatedMemberDeclaration(SyntaxNode node, EditKind editKind, [NotNullWhen(true)] out SyntaxNode? declaration)
{
if (node.Kind() is SyntaxKind.Parameter or SyntaxKind.TypeParameter)
{
Contract.ThrowIfFalse(node.Parent is (kind:
SyntaxKind.ParameterList or
SyntaxKind.TypeParameterList or
SyntaxKind.BracketedParameterList));
declaration = node.Parent.Parent!;
return true;
}
// For deletes, we don't associate accessors with their parents, as deleting accessors is allowed
if (editKind != EditKind.Delete &&
node.Parent?.Parent is (kind:
SyntaxKind.PropertyDeclaration or
SyntaxKind.IndexerDeclaration or
SyntaxKind.EventDeclaration))
{
declaration = node.Parent.Parent;
return true;
}
declaration = null;
return false;
}
internal override bool IsDeclarationWithInitializer(SyntaxNode declaration)
=> declaration is VariableDeclaratorSyntax { Initializer: not null } or PropertyDeclarationSyntax { Initializer: not null };
internal override bool IsRecordPrimaryConstructorParameter(SyntaxNode declaration)
=> declaration is ParameterSyntax { Parent: ParameterListSyntax { Parent: RecordDeclarationSyntax } };
private static bool IsPropertyDeclarationMatchingPrimaryConstructorParameter(SyntaxNode declaration, INamedTypeSymbol newContainingType)
{
if (newContainingType.IsRecord &&
declaration is PropertyDeclarationSyntax { Identifier.ValueText: var name })
{
// We need to use symbol information to find the primary constructor, because it could be in another file if the type is partial
foreach (var reference in newContainingType.DeclaringSyntaxReferences)
{
// Since users can define as many constructors as they like, going back to syntax to find the parameter list
// in the record declaration is the simplest way to check if there is a matching parameter
if (reference.GetSyntax() is RecordDeclarationSyntax record &&
record.ParameterList is not null &&
record.ParameterList.Parameters.Any(p => p.Identifier.ValueText.Equals(name)))
{
return true;
}
}
}
return false;
}
internal override bool IsPropertyAccessorDeclarationMatchingPrimaryConstructorParameter(SyntaxNode declaration, INamedTypeSymbol newContainingType, out bool isFirstAccessor)
{
isFirstAccessor = false;
if (declaration is AccessorDeclarationSyntax { Parent: AccessorListSyntax { Parent: PropertyDeclarationSyntax property } list } &&
IsPropertyDeclarationMatchingPrimaryConstructorParameter(property, newContainingType))
{
isFirstAccessor = list.Accessors[0] == declaration;
return true;
}
return false;
}
internal override bool IsConstructorWithMemberInitializers(SyntaxNode constructorDeclaration)
=> constructorDeclaration is ConstructorDeclarationSyntax ctor && (ctor.Initializer == null || ctor.Initializer.IsKind(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(SyntaxReference reference, CancellationToken cancellationToken)
=> reference.GetSyntax(cancellationToken);
protected override OneOrMany<(ISymbol? oldSymbol, ISymbol? newSymbol, EditKind editKind)> GetSymbolEdits(
EditKind editKind,
SyntaxNode? oldNode,
SyntaxNode? newNode,
SemanticModel? oldModel,
SemanticModel newModel,
IReadOnlyDictionary<SyntaxNode, EditKind> editMap,
CancellationToken cancellationToken)
{
var oldSymbol = (oldNode != null) ? GetSymbolForEdit(oldNode, oldModel!, cancellationToken) : null;
var newSymbol = (newNode != null) ? GetSymbolForEdit(newNode, newModel, cancellationToken) : null;
switch (editKind)
{
case EditKind.Reorder:
Contract.ThrowIfNull(oldNode);
if (oldNode is ParameterSyntax)
{
Debug.Assert(oldSymbol is IParameterSymbol);
Debug.Assert(newSymbol is IParameterSymbol);
// When parameters are reordered, we issue an update edit for the containing method
return new OneOrMany<(ISymbol?, ISymbol?, EditKind)>((oldSymbol.ContainingSymbol, newSymbol.ContainingSymbol, EditKind.Update));
}
else if (IsGlobalStatement(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
return new OneOrMany<(ISymbol?, ISymbol?, EditKind)>((oldSymbol, newSymbol, EditKind.Update));
}
// Otherwise, we don't do any semantic checks for reordering
// and we don't need to report them to the compiler either.
// Consider: Currently symbol ordering changes are not reflected in metadata (Reflection will report original order).
// Consider: Reordering of fields is not allowed since it changes the layout of the type.
// This ordering should however not matter unless the type has explicit layout so we might want to allow it.
// We do not check changes to the order if they occur across multiple documents (the containing type is partial).
Debug.Assert(!IsDeclarationWithInitializer(oldNode!) && !IsDeclarationWithInitializer(newNode!));
return OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty;
case EditKind.Update:
Contract.ThrowIfNull(oldNode);
Contract.ThrowIfNull(newNode);
Contract.ThrowIfNull(oldModel);
// Certain updates of a property/indexer node affects its accessors.
// Return all affected symbols for these updates.
// 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
if (oldNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null } ||
newNode is PropertyDeclarationSyntax { ExpressionBody: not null } or IndexerDeclarationSyntax { ExpressionBody: not null })
{
Debug.Assert(oldSymbol is IPropertySymbol);
Debug.Assert(newSymbol is IPropertySymbol);
var oldGetterSymbol = ((IPropertySymbol)oldSymbol).GetMethod;
var newGetterSymbol = ((IPropertySymbol)newSymbol).GetMethod;
return OneOrMany.Create(ImmutableArray.Create((oldSymbol, newSymbol, editKind), (oldGetterSymbol, newGetterSymbol, editKind)));
}
// 2) Property/indexer declarations differ in readonly keyword.
if (oldNode is PropertyDeclarationSyntax oldProperty && newNode is PropertyDeclarationSyntax newProperty && DiffersInReadOnlyModifier(oldProperty.Modifiers, newProperty.Modifiers) ||
oldNode is IndexerDeclarationSyntax oldIndexer && newNode is IndexerDeclarationSyntax newIndexer && DiffersInReadOnlyModifier(oldIndexer.Modifiers, newIndexer.Modifiers))
{
Debug.Assert(oldSymbol is IPropertySymbol);
Debug.Assert(newSymbol is IPropertySymbol);
var oldPropertySymbol = (IPropertySymbol)oldSymbol;
var newPropertySymbol = (IPropertySymbol)newSymbol;
using var _ = ArrayBuilder<(ISymbol?, ISymbol?, EditKind)>.GetInstance(out var builder);
builder.Add((oldPropertySymbol, newPropertySymbol, editKind));
if (oldPropertySymbol.GetMethod != null && newPropertySymbol.GetMethod != null && oldPropertySymbol.GetMethod.IsReadOnly != newPropertySymbol.GetMethod.IsReadOnly)
{
builder.Add((oldPropertySymbol.GetMethod, newPropertySymbol.GetMethod, editKind));
}
if (oldPropertySymbol.SetMethod != null && newPropertySymbol.SetMethod != null && oldPropertySymbol.SetMethod.IsReadOnly != newPropertySymbol.SetMethod.IsReadOnly)
{
builder.Add((oldPropertySymbol.SetMethod, newPropertySymbol.SetMethod, editKind));
}
return OneOrMany.Create(builder.ToImmutable());
}
static bool DiffersInReadOnlyModifier(SyntaxTokenList oldModifiers, SyntaxTokenList newModifiers)
=> (oldModifiers.IndexOf(SyntaxKind.ReadOnlyKeyword) >= 0) != (newModifiers.IndexOf(SyntaxKind.ReadOnlyKeyword) >= 0);
// Change in attributes or modifiers of a field affects all its variable declarations.
if (oldNode is BaseFieldDeclarationSyntax oldField && newNode is BaseFieldDeclarationSyntax newField)
{
return GetFieldSymbolUpdates(oldField.Declaration.Variables, newField.Declaration.Variables);
}
// Chnage in type of a field affects all its variable declarations.
if (oldNode is VariableDeclarationSyntax oldVariableDeclaration && newNode is VariableDeclarationSyntax newVariableDeclaration)
{
return GetFieldSymbolUpdates(oldVariableDeclaration.Variables, newVariableDeclaration.Variables);
}
OneOrMany<(ISymbol?, ISymbol?, EditKind)> GetFieldSymbolUpdates(SeparatedSyntaxList<VariableDeclaratorSyntax> oldVariables, SeparatedSyntaxList<VariableDeclaratorSyntax> newVariables)
{
if (oldVariables.Count == 1 && newVariables.Count == 1)
{
return OneOrMany.Create((oldModel.GetDeclaredSymbol(oldVariables[0], cancellationToken), newModel.GetDeclaredSymbol(newVariables[0], cancellationToken), EditKind.Update));
}
var result = from oldVariable in oldVariables
join newVariable in newVariables on oldVariable.Identifier.Text equals newVariable.Identifier.Text
select (oldModel.GetDeclaredSymbol(oldVariable, cancellationToken), newModel.GetDeclaredSymbol(newVariable, cancellationToken), EditKind.Update);
return OneOrMany.Create(result.ToImmutableArray());
}
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 OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty;
}
}
// 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;
return OneOrMany.Create(ImmutableArray.Create((oldSymbol, newSymbol, editKind), (oldGetterSymbol, newGetterSymbol, editKind)));
}
// Inserting/deleting a type parameter constraint should result in an update of the corresponding type parameter symbol:
if (node.IsKind(SyntaxKind.TypeParameterConstraintClause))
{
return OneOrMany.Create(ImmutableArray.Create((oldSymbol, newSymbol, EditKind.Update)));
}
// Inserting/deleting a global statement should result in an update of the implicit main method:
if (node.IsKind(SyntaxKind.GlobalStatement))
{
return OneOrMany.Create(ImmutableArray.Create((oldSymbol, newSymbol, EditKind.Update)));
}
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));
return oldNode.IsKind(SyntaxKind.LocalFunctionStatement)
? OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty
: OneOrMany.Create((oldSymbol, newSymbol, editKind));
}
return (editKind == EditKind.Delete ? oldSymbol : newSymbol) is null ?
OneOrMany<(ISymbol?, ISymbol?, EditKind)>.Empty : new OneOrMany<(ISymbol?, ISymbol?, EditKind)>((oldSymbol, newSymbol, editKind));
}
private static 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);
}
var symbol = model.GetDeclaredSymbol(node, cancellationToken);
// TODO: this is incorrect (https://github.com/dotnet/roslyn/issues/54800)
// Ignore partial method definition parts.
// Partial method that does not have implementation part is not emitted to metadata.
// Partial method without a definition part is a compilation error.
if (symbol is IMethodSymbol { IsPartialDefinition: true })
{
return null;
}
return symbol;
}
private static bool SupportsMove(SyntaxNode node)
=> node.IsKind(SyntaxKind.LocalFunctionStatement) ||
IsTypeDeclaration(node) ||
node is BaseNamespaceDeclarationSyntax;
internal override bool ContainsLambda(SyntaxNode declaration)
=> declaration.DescendantNodes().Any(LambdaUtilities.IsLambda);
internal override bool IsLambda(SyntaxNode node)
=> LambdaUtilities.IsLambda(node);
internal override bool IsLocalFunction(SyntaxNode node)
=> node.IsKind(SyntaxKind.LocalFunctionStatement);
internal override bool IsNestedFunction(SyntaxNode node)
=> node is AnonymousFunctionExpressionSyntax or LocalFunctionStatementSyntax;
internal override bool TryGetLambdaBodies(SyntaxNode node, [NotNullWhen(true)] out SyntaxNode? body1, out SyntaxNode? body2)
=> LambdaUtilities.TryGetLambdaBodies(node, out body1, out body2);
internal override SyntaxNode GetLambda(SyntaxNode lambdaBody)
=> LambdaUtilities.GetLambda(lambdaBody);
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 MemberSignaturesEquivalent(oldQueryClauseInfo.CastInfo.Symbol, newQueryClauseInfo.CastInfo.Symbol) &&
MemberSignaturesEquivalent(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 MemberSignaturesEquivalent(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 ||
MemberSignaturesEquivalent(oldSelectInfo.Symbol, newSelectInfo.Symbol);
case SyntaxKind.GroupClause:
var oldGroupByInfo = oldModel.GetSymbolInfo(oldNode, cancellationToken);
var newGroupByInfo = newModel.GetSymbolInfo(newNode, cancellationToken);
return MemberSignaturesEquivalent(oldGroupByInfo.Symbol, newGroupByInfo.Symbol, GroupBySignatureComparer);
default:
return true;
}
}
private static bool GroupBySignatureComparer(ImmutableArray<IParameterSymbol> oldParameters, ITypeSymbol oldReturnType, ImmutableArray<IParameterSymbol> newParameters, ITypeSymbol newReturnType)
{
// 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 (!TypesEquivalent(oldReturnType, newReturnType, exact: false))
{
return false;
}
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:
return default(TextSpan);
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.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.AttributeList:
var attributeList = (AttributeListSyntax)node;
return attributeList.Span;
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:
return ((ParenthesizedLambdaExpressionSyntax)node).ParameterList.Span;
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 : CSharpFeaturesResources.struct_,
TypeKind.Class => symbol.IsRecord ? CSharpFeaturesResources.record_ : FeaturesResources.class_,
_ => 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,
_ => 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:
case SyntaxKind.GlobalStatement:
return CSharpFeaturesResources.global_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 CSharpFeaturesResources.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.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 CSharpFeaturesResources.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: new[] { 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.PropertyDeclaration:
case SyntaxKind.FieldDeclaration:
case SyntaxKind.EventFieldDeclaration:
case SyntaxKind.VariableDeclarator:
// Maybe we could allow changing order of field declarations unless the containing type layout is sequential.
ReportError(RudeEditKind.Move);
return;
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;
}
}
public void ClassifyDeclarationBodyRudeUpdates(SyntaxNode newDeclarationOrBody)
{
foreach (var node in newDeclarationOrBody.DescendantNodesAndSelf(LambdaUtilities.IsNotLambda))
{
switch (node.Kind())
{
case SyntaxKind.StackAllocArrayCreationExpression:
case SyntaxKind.ImplicitStackAllocArrayCreationExpression:
ReportError(RudeEditKind.StackAllocUpdate, node, _newNode);
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 void ReportMemberBodyUpdateRudeEdits(ArrayBuilder<RudeEditDiagnostic> diagnostics, SyntaxNode newMember, TextSpan? span)
{
var classifier = new EditClassifier(this, diagnostics, oldNode: null, newMember, EditKind.Update, span: span);
classifier.ClassifyDeclarationBodyRudeUpdates(newMember);
}
#endregion
#region Semantic Rude Edits
internal override void ReportInsertedMemberSymbolRudeEdits(ArrayBuilder<RudeEditDiagnostic> diagnostics, ISymbol newSymbol, SyntaxNode newNode, bool insertingIntoExistingContainingType)
{
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 a member into an existing generic type is not allowed.
{ ContainingType.Arity: > 0 } and not INamedTypeSymbol
=> RudeEditKind.InsertIntoGenericType,
// Inserting virtual or interface member into an existing type is not allowed.
{ IsVirtual: true } or { IsOverride: true } or { IsAbstract: true } and not INamedTypeSymbol
=> RudeEditKind.InsertVirtual,
// Inserting generic method into an existing type is not allowed.
IMethodSymbol { Arity: > 0 }
=> RudeEditKind.InsertGenericMethod,
// 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 } and not INamedTypeSymbol
=> 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: new[] { 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, bool isNonLeaf)
{
var result = new List<SyntaxNode>();
var current = node;
while (current != null)
{
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;
}
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);
protected override void GetStateMachineInfo(SyntaxNode body, out ImmutableArray<SyntaxNode> suspensionPoints, out StateMachineKinds kinds)
{
suspensionPoints = SyntaxUtilities.GetSuspensionPoints(body).ToImmutableArray();
kinds = StateMachineKinds.None;
if (SyntaxUtilities.IsIterator(body))
{
kinds |= StateMachineKinds.Iterator;
}
if (SyntaxUtilities.IsAsyncDeclaration(body.Parent))
{
kinds |= StateMachineKinds.Async;
}
}
internal override void ReportStateMachineSuspensionPointRudeEdits(ArrayBuilder<RudeEditDiagnostic> diagnostics, 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))
{
diagnostics.Add(new RudeEditDiagnostic(RudeEditKind.AwaitStatementUpdate, newContainingStatementPart.Span));
}
}
}
internal override void ReportStateMachineSuspensionPointDeletedRudeEdit(ArrayBuilder<RudeEditDiagnostic> diagnostics, Match<SyntaxNode> match, SyntaxNode deletedSuspensionPoint)
{
// Handle deletion of await keyword from await foreach statement.
if (deletedSuspensionPoint is CommonForEachStatementSyntax deletedForeachStatement &&
match.Matches.TryGetValue(deletedSuspensionPoint, out var newForEachStatement) &&
newForEachStatement is CommonForEachStatementSyntax &&
deletedForeachStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.ChangingFromAsynchronousToSynchronous,
GetDiagnosticSpan(newForEachStatement, EditKind.Update),
newForEachStatement,
new[] { GetDisplayName(newForEachStatement, EditKind.Update) }));
return;
}
// Handle deletion of await keyword from await using declaration.
if (deletedSuspensionPoint.IsKind(SyntaxKind.VariableDeclarator) &&
match.Matches.TryGetValue(deletedSuspensionPoint.Parent!.Parent!, out var newLocalDeclaration) &&
!((LocalDeclarationStatementSyntax)newLocalDeclaration).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.ChangingFromAsynchronousToSynchronous,
GetDiagnosticSpan(newLocalDeclaration, EditKind.Update),
newLocalDeclaration,
new[] { GetDisplayName(newLocalDeclaration, EditKind.Update) }));
return;
}
base.ReportStateMachineSuspensionPointDeletedRudeEdit(diagnostics, match, deletedSuspensionPoint);
}
internal override void ReportStateMachineSuspensionPointInsertedRudeEdit(ArrayBuilder<RudeEditDiagnostic> diagnostics, Match<SyntaxNode> match, SyntaxNode insertedSuspensionPoint, bool aroundActiveStatement)
{
// Handle addition of await keyword to foreach statement.
if (insertedSuspensionPoint is CommonForEachStatementSyntax insertedForEachStatement &&
match.ReverseMatches.TryGetValue(insertedSuspensionPoint, out var oldNode) &&
oldNode is CommonForEachStatementSyntax oldForEachStatement &&
!oldForEachStatement.AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.Insert,
insertedForEachStatement.AwaitKeyword.Span,
insertedForEachStatement,
new[] { insertedForEachStatement.AwaitKeyword.ToString() }));
return;
}
// Handle addition of using keyword to using declaration.
if (insertedSuspensionPoint.IsKind(SyntaxKind.VariableDeclarator) &&
match.ReverseMatches.TryGetValue(insertedSuspensionPoint.Parent!.Parent!, out var oldLocalDeclaration) &&
!((LocalDeclarationStatementSyntax)oldLocalDeclaration).AwaitKeyword.IsKind(SyntaxKind.AwaitKeyword))
{
var newLocalDeclaration = (LocalDeclarationStatementSyntax)insertedSuspensionPoint!.Parent!.Parent!;
diagnostics.Add(new RudeEditDiagnostic(
RudeEditKind.Insert,
newLocalDeclaration.AwaitKeyword.Span,
newLocalDeclaration,
new[] { newLocalDeclaration.AwaitKeyword.ToString() }));
return;
}
base.ReportStateMachineSuspensionPointInsertedRudeEdit(diagnostics, match, insertedSuspensionPoint, aroundActiveStatement);
}
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,
Match<SyntaxNode> match,
SyntaxNode oldActiveStatement,
SyntaxNode newActiveStatement,
bool isNonLeaf)
{
ReportRudeEditsForSwitchWhenClauses(diagnostics, oldActiveStatement, newActiveStatement);
ReportRudeEditsForAncestorsDeclaringInterStatementTemps(diagnostics, match, oldActiveStatement, newActiveStatement);
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,
new[] { 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,
Match<SyntaxNode> match,
SyntaxNode oldActiveStatement,
SyntaxNode newActiveStatement)
{
// 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, match, n => n.IsKind(SyntaxKind.LockStatement), oldActiveStatement, newActiveStatement,
areEquivalent: AreEquivalentActiveStatements,
areSimilar: null);
ReportUnmatchedStatements<FixedStatementSyntax>(diagnostics, match, n => n.IsKind(SyntaxKind.FixedStatement), oldActiveStatement, newActiveStatement,
areEquivalent: AreEquivalentActiveStatements,
areSimilar: (n1, n2) => DeclareSameIdentifiers(n1.Declaration.Variables, n2.Declaration.Variables));
// Using statements with declaration do not introduce compiler generated temporary.
ReportUnmatchedStatements<UsingStatementSyntax>(
diagnostics,
match,
n => n is UsingStatementSyntax usingStatement && usingStatement.Declaration is null,
oldActiveStatement,
newActiveStatement,
areEquivalent: AreEquivalentActiveStatements,
areSimilar: null);
ReportUnmatchedStatements<CommonForEachStatementSyntax>(
diagnostics,
match,
n => n.IsKind(SyntaxKind.ForEachStatement) || n.IsKind(SyntaxKind.ForEachVariableStatement),
oldActiveStatement,
newActiveStatement,
areEquivalent: AreEquivalentActiveStatements,
areSimilar: AreSimilarActiveStatements);
}
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
protected override bool IsRudeEditDueToPrimaryConstructor(ISymbol symbol, CancellationToken cancellationToken)
{
switch (symbol.Kind)
{
case SymbolKind.NamedType:
{
return IsTypeWithPrimaryConstructor(symbol, cancellationToken);
}
case SymbolKind.Parameter:
{
var container = symbol.ContainingSymbol;
if (container is IMethodSymbol { IsImplicitlyDeclared: false, MethodKind: MethodKind.Constructor })
{
foreach (var syntaxReference in container.DeclaringSyntaxReferences)
{
if (syntaxReference.GetSyntax(cancellationToken) is
ClassDeclarationSyntax { ParameterList: not null } or
StructDeclarationSyntax { ParameterList: not null })
{
return true;
}
}
}
}
break;
default:
{
return IsTypeWithPrimaryConstructor(symbol.ContainingSymbol, cancellationToken);
}
}
return false;
static bool IsTypeWithPrimaryConstructor(ISymbol container, CancellationToken cancellationToken)
{
if (container is { Kind: SymbolKind.NamedType, IsImplicitlyDeclared: false })
{
foreach (var syntaxReference in container.DeclaringSyntaxReferences)
{
if (syntaxReference.GetSyntax(cancellationToken) is
ClassDeclarationSyntax { ParameterList: not null } or
StructDeclarationSyntax { ParameterList: not null })
{
return true;
}
}
}
return false;
}
}
}
}
|