/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharp/Portable/EditAndContinue/SyntaxComparer.cs
1 683 строки
66 KB
Cyrus Najmabadi
use patterns
06 янв 2026, 06:41
06 янв 2026, 06:41
3a10394
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Differencing; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue; /// <summary> /// Creates a syntax comparer /// </summary> /// <param name="oldRoot">The root node to start comparisons from</param> /// <param name="newRoot">The new root node to compare against</param> /// <param name="oldRootChildren">Child nodes that should always be compared</param> /// <param name="newRootChildren">New child nodes to compare against</param> /// <param name="compareStatementSyntax">Whether this comparer is in "statement mode"</param> internal sealed class SyntaxComparer( SyntaxNode? oldRoot, SyntaxNode? newRoot, IEnumerable<SyntaxNode>? oldRootChildren, IEnumerable<SyntaxNode>? newRootChildren, bool compareStatementSyntax) : AbstractSyntaxComparer(oldRoot, newRoot, oldRootChildren, newRootChildren, compareStatementSyntax) { internal static readonly SyntaxComparer TopLevel = new(null, null, null, null, compareStatementSyntax: false); internal static readonly SyntaxComparer Statement = new(null, null, null, null, compareStatementSyntax: true); protected override bool IsLambdaBodyStatementOrExpression(SyntaxNode node) => LambdaUtilities.IsLambdaBodyStatementOrExpression(node); #region Labels /// <summary> /// Assumptions: /// - Each listed label corresponds to one or more syntax kinds. /// - Nodes with same labels might produce Update edits, nodes with different labels don't. /// - If <see cref="TiedToAncestor(Label)"/> is true for a label then all its possible parent labels must precede the label. /// (i.e. both MethodDeclaration and TypeDeclaration must precede TypeParameter label). /// - All descendants of a node whose kind is listed here will be ignored regardless of their labels /// </summary> internal enum Label { // Top level syntax kinds CompilationUnit, GlobalStatement, NamespaceDeclaration, ExternAliasDirective, // tied to parent UsingDirective, // tied to parent TypeDeclaration, EnumDeclaration, ExtensionBlockDeclaration, // tied to parent BaseList, // tied to parent PrimaryConstructorBase, // tied to parent DelegateDeclaration, FieldDeclaration, // tied to parent FieldVariableDeclaration, // tied to parent FieldVariableDeclarator, // tied to parent MethodDeclaration, // tied to parent OperatorDeclaration, // tied to parent ConversionOperatorDeclaration, // tied to parent ConstructorDeclaration, // tied to parent DestructorDeclaration, // tied to parent PropertyDeclaration, // tied to parent IndexerDeclaration, // tied to parent EventDeclaration, // tied to parent EnumMemberDeclaration, // tied to parent ArrowExpressionClause, // tied to parent AccessorList, // tied to parent AccessorDeclaration, // tied to parent // Statement syntax kinds Block, CheckedStatement, UnsafeStatement, TryStatement, CatchClause, // tied to parent CatchDeclaration, // tied to parent CatchFilterClause, // tied to parent FinallyClause, // tied to parent ForStatement, ForStatementPart, // tied to parent ForEachStatement, UsingStatementWithExpression, UsingStatementWithDeclarations, FixedStatement, LockStatement, WhileStatement, DoStatement, IfStatement, ElseClause, // tied to parent SwitchStatement, SwitchSection, CasePatternSwitchLabel, // tied to parent SwitchExpression, SwitchExpressionArm, // tied to parent WhenClause, // tied to parent YieldReturnStatement, // tied to parent YieldBreakStatement, // tied to parent GotoStatement, GotoCaseStatement, BreakContinueStatement, ReturnThrowStatement, ExpressionStatement, LabeledStatement, // TODO: // Ideally we could declare LocalVariableDeclarator tied to the first enclosing node that defines local scope (block, foreach, etc.) // Also consider handling LocalDeclarationStatement as just a bag of variable declarators, // so that variable declarators contained in one can be matched with variable declarators contained in the other. LocalDeclarationStatement, // tied to parent LocalVariableDeclaration, // tied to parent LocalVariableDeclarator, // tied to parent SingleVariableDesignation, AwaitExpression, NestedFunction, FromClause, QueryBody, FromClauseLambda, // tied to parent LetClauseLambda, // tied to parent WhereClauseLambda, // tied to parent OrderByClause, // tied to parent OrderingLambda, // tied to parent SelectClauseLambda, // tied to parent JoinClauseLambda, // tied to parent JoinIntoClause, // tied to parent GroupClauseLambda, // tied to parent QueryContinuation, // tied to parent // Syntax kinds that are common to both statement and top level TypeParameterList, // tied to parent TypeParameterConstraintClause, // tied to parent TypeParameter, // tied to parent ParameterList, // tied to parent BracketedParameterList, // tied to parent Parameter, // tied to parent AttributeList, // tied to parent Attribute, // tied to parent // helpers: Count, Ignored = IgnoredNode } /// <summary> /// Return 1 if it is desirable to report two edits (delete and insert) rather than a move edit /// when the node changes its parent. /// </summary> private static int TiedToAncestor(Label label) { switch (label) { // Top level syntax case Label.ExternAliasDirective: case Label.UsingDirective: case Label.FieldDeclaration: case Label.FieldVariableDeclaration: case Label.FieldVariableDeclarator: case Label.MethodDeclaration: case Label.OperatorDeclaration: case Label.ConversionOperatorDeclaration: case Label.ConstructorDeclaration: case Label.DestructorDeclaration: case Label.PropertyDeclaration: case Label.ArrowExpressionClause: case Label.IndexerDeclaration: case Label.EventDeclaration: case Label.EnumMemberDeclaration: case Label.BaseList: case Label.AccessorDeclaration: case Label.AccessorList: case Label.TypeParameterList: case Label.TypeParameter: case Label.TypeParameterConstraintClause: case Label.ParameterList: case Label.BracketedParameterList: case Label.Parameter: case Label.AttributeList: case Label.Attribute: case Label.ExtensionBlockDeclaration: return 1; // Statement syntax case Label.LocalDeclarationStatement: case Label.LocalVariableDeclaration: case Label.LocalVariableDeclarator: case Label.GotoCaseStatement: case Label.BreakContinueStatement: case Label.ElseClause: case Label.CatchClause: case Label.CatchDeclaration: case Label.CatchFilterClause: case Label.FinallyClause: case Label.ForStatementPart: case Label.YieldReturnStatement: case Label.YieldBreakStatement: case Label.FromClauseLambda: case Label.LetClauseLambda: case Label.WhereClauseLambda: case Label.OrderByClause: case Label.OrderingLambda: case Label.SelectClauseLambda: case Label.JoinClauseLambda: case Label.JoinIntoClause: case Label.GroupClauseLambda: case Label.QueryContinuation: case Label.CasePatternSwitchLabel: case Label.WhenClause: case Label.SwitchExpressionArm: return 1; default: return 0; } } internal override int Classify(int kind, SyntaxNode? node, out bool isLeaf) => (int)Classify((SyntaxKind)kind, node, out isLeaf); internal Label Classify(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) { isLeaf = false; // If the node is a for loop Initializer, Condition, or Incrementor expression we label it as "ForStatementPart". // We need to capture it in the match since these expressions can be "active statements" and as such we need to map them. // // The parent is not available only when comparing nodes for value equality. if (node != null && node.Parent.IsKind(SyntaxKind.ForStatement) && node is ExpressionSyntax) { return Label.ForStatementPart; } // ************************************ // Top and statement syntax // ************************************ // These nodes can appear during top level and statement processing, so we put them in this first // switch for simplicity. Statement specific, and top level specific cases are handled below. switch (kind) { case SyntaxKind.CompilationUnit: return Label.CompilationUnit; case SyntaxKind.TypeParameterList: return Label.TypeParameterList; case SyntaxKind.TypeParameterConstraintClause: return Label.TypeParameterConstraintClause; case SyntaxKind.TypeParameter: // not a leaf because an attribute may be applied return Label.TypeParameter; case SyntaxKind.BracketedParameterList: return Label.BracketedParameterList; case SyntaxKind.ParameterList: return Label.ParameterList; case SyntaxKind.Parameter: return Label.Parameter; case SyntaxKind.ConstructorDeclaration: // Root when matching constructor bodies. return Label.ConstructorDeclaration; } if (_compareStatementSyntax) { return ClassifyStatementSyntax(kind, node, out isLeaf); } return ClassifyTopSyntax(kind, node, out isLeaf); } private static Label ClassifyStatementSyntax(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) { isLeaf = false; // ************************************ // Statement syntax // ************************************ // These nodes could potentially be seen as top level syntax, but we only want them labelled // during statement syntax so they have to be kept separate. // // For example when top level sees something like this: // // private int X => new Func(() => { return 1 })(); // // It needs to go through the entire lambda to know if that property def has changed // but if we start labelling things, like ReturnStatement in the above example, then // it will stop. Given that a block bodied lambda can have any statements a user likes // the whole set has to be dealt with separately. switch (kind) { // Notes: // A descendant of a leaf node may be a labeled node that we don't want to visit if // we are comparing its parent node (used for lambda bodies). // // Expressions are ignored but they may contain nodes that should be matched by tree comparer. // (e.g. lambdas, declaration expressions). Descending to these nodes is handled in EnumerateChildren. case SyntaxKind.NamespaceDeclaration: case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.StructDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: // These declarations can come after global statements so we want to stop statement matching // because no global statements can come after them isLeaf = true; return Label.Ignored; case SyntaxKind.LocalDeclarationStatement: return Label.LocalDeclarationStatement; case SyntaxKind.SingleVariableDesignation: return Label.SingleVariableDesignation; case SyntaxKind.LabeledStatement: return Label.LabeledStatement; case SyntaxKind.EmptyStatement: isLeaf = true; return Label.ExpressionStatement; case SyntaxKind.GotoStatement: isLeaf = true; return Label.GotoStatement; case SyntaxKind.GotoCaseStatement: case SyntaxKind.GotoDefaultStatement: isLeaf = true; return Label.GotoCaseStatement; case SyntaxKind.BreakStatement: case SyntaxKind.ContinueStatement: isLeaf = true; return Label.BreakContinueStatement; case SyntaxKind.ReturnStatement: case SyntaxKind.ThrowStatement: return Label.ReturnThrowStatement; case SyntaxKind.ExpressionStatement: return Label.ExpressionStatement; case SyntaxKind.YieldBreakStatement: // yield break is distinct from yield return as it does not suspend the state machine in a resumable state return Label.YieldBreakStatement; case SyntaxKind.YieldReturnStatement: return Label.YieldReturnStatement; case SyntaxKind.DoStatement: return Label.DoStatement; case SyntaxKind.WhileStatement: return Label.WhileStatement; case SyntaxKind.ForStatement: return Label.ForStatement; case SyntaxKind.ForEachVariableStatement: case SyntaxKind.ForEachStatement: return Label.ForEachStatement; case SyntaxKind.UsingStatement: // We need to distinguish using statements with expression or single variable declaration from ones with multiple variable declarations. // The former generate a single try-finally block, the latter one for each variable. The finally blocks need to match since they // affect state machine state matching. For simplicity we do not match single-declaration to expression, we just treat usings // with declarations entirely separately from usings with expressions. // // The parent is not available only when comparing nodes for value equality. // In that case it doesn't matter what label the node has as long as it has some. return node is UsingStatementSyntax { Declaration: not null } ? Label.UsingStatementWithDeclarations : Label.UsingStatementWithExpression; case SyntaxKind.FixedStatement: return Label.FixedStatement; case SyntaxKind.CheckedStatement: case SyntaxKind.UncheckedStatement: return Label.CheckedStatement; case SyntaxKind.UnsafeStatement: return Label.UnsafeStatement; case SyntaxKind.LockStatement: return Label.LockStatement; case SyntaxKind.IfStatement: return Label.IfStatement; case SyntaxKind.ElseClause: return Label.ElseClause; case SyntaxKind.SwitchStatement: return Label.SwitchStatement; case SyntaxKind.SwitchSection: return Label.SwitchSection; case SyntaxKind.CaseSwitchLabel: case SyntaxKind.DefaultSwitchLabel: // Switch labels are included in the "value" of the containing switch section. // We don't need to analyze case expressions. isLeaf = true; return Label.Ignored; case SyntaxKind.WhenClause: return Label.WhenClause; case SyntaxKind.CasePatternSwitchLabel: return Label.CasePatternSwitchLabel; case SyntaxKind.SwitchExpression: return Label.SwitchExpression; case SyntaxKind.SwitchExpressionArm: return Label.SwitchExpressionArm; case SyntaxKind.TryStatement: return Label.TryStatement; case SyntaxKind.CatchClause: return Label.CatchClause; case SyntaxKind.CatchDeclaration: // the declarator of the exception variable return Label.CatchDeclaration; case SyntaxKind.CatchFilterClause: return Label.CatchFilterClause; case SyntaxKind.FinallyClause: return Label.FinallyClause; case SyntaxKind.FromClause: // The first from clause of a query is not a lambda. // We have to assign it a label different from "FromClauseLambda" // so that we won't match lambda-from to non-lambda-from. // // Since FromClause declares range variables we need to include it in the map, // so that we are able to map range variable declarations. // Therefore we assign it a dedicated label. // // The parent is not available only when comparing nodes for value equality. // In that case it doesn't matter what label the node has as long as it has some. if (node == null || node.Parent.IsKind(SyntaxKind.QueryExpression)) { return Label.FromClause; } return Label.FromClauseLambda; case SyntaxKind.QueryBody: return Label.QueryBody; case SyntaxKind.QueryContinuation: return Label.QueryContinuation; case SyntaxKind.LetClause: return Label.LetClauseLambda; case SyntaxKind.WhereClause: return Label.WhereClauseLambda; case SyntaxKind.OrderByClause: return Label.OrderByClause; case SyntaxKind.AscendingOrdering: case SyntaxKind.DescendingOrdering: return Label.OrderingLambda; case SyntaxKind.SelectClause: return Label.SelectClauseLambda; case SyntaxKind.JoinClause: return Label.JoinClauseLambda; case SyntaxKind.JoinIntoClause: return Label.JoinIntoClause; case SyntaxKind.GroupClause: return Label.GroupClauseLambda; case SyntaxKind.IdentifierName: case SyntaxKind.QualifiedName: case SyntaxKind.GenericName: case SyntaxKind.TypeArgumentList: case SyntaxKind.AliasQualifiedName: case SyntaxKind.PredefinedType: case SyntaxKind.PointerType: case SyntaxKind.NullableType: case SyntaxKind.TupleType: case SyntaxKind.RefType: case SyntaxKind.OmittedTypeArgument: case SyntaxKind.NameColon: case SyntaxKind.OmittedArraySizeExpression: case SyntaxKind.ThisExpression: case SyntaxKind.BaseExpression: case SyntaxKind.ArgListExpression: case SyntaxKind.NumericLiteralExpression: case SyntaxKind.StringLiteralExpression: case SyntaxKind.CharacterLiteralExpression: case SyntaxKind.TrueLiteralExpression: case SyntaxKind.FalseLiteralExpression: case SyntaxKind.NullLiteralExpression: case SyntaxKind.TypeOfExpression: case SyntaxKind.SizeOfExpression: case SyntaxKind.DefaultExpression: case SyntaxKind.ConstantPattern: case SyntaxKind.DiscardDesignation: // can't contain a lambda/await/anonymous type: isLeaf = true; return Label.Ignored; case SyntaxKind.AwaitExpression: return Label.AwaitExpression; case SyntaxKind.ParenthesizedLambdaExpression: case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.AnonymousMethodExpression: case SyntaxKind.LocalFunctionStatement: return Label.NestedFunction; case SyntaxKind.VariableDeclaration: return Label.LocalVariableDeclaration; case SyntaxKind.VariableDeclarator: return Label.LocalVariableDeclarator; case SyntaxKind.Block: return Label.Block; } // If we got this far, its an unlabelled node. Since just about any node can // contain a lambda, isLeaf must be false for statement syntax. return Label.Ignored; } private static Label ClassifyTopSyntax(SyntaxKind kind, SyntaxNode? node, out bool isLeaf) { isLeaf = false; // ************************************ // Top syntax // ************************************ // More the most part these nodes will only appear in top syntax but its easier to // keep them separate so we can more easily discern was is shared, above. switch (kind) { case SyntaxKind.GlobalStatement: isLeaf = true; return Label.GlobalStatement; case SyntaxKind.ExternAliasDirective: isLeaf = true; return Label.ExternAliasDirective; case SyntaxKind.UsingDirective: isLeaf = true; return Label.UsingDirective; case SyntaxKind.NamespaceDeclaration: case SyntaxKind.FileScopedNamespaceDeclaration: return Label.NamespaceDeclaration; case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: return Label.TypeDeclaration; case SyntaxKind.BaseList: return Label.BaseList; case SyntaxKind.PrimaryConstructorBaseType: // For top syntax, primary constructor base initializer is a leaf node isLeaf = true; return Label.PrimaryConstructorBase; case SyntaxKind.MethodDeclaration: return Label.MethodDeclaration; case SyntaxKind.EnumDeclaration: return Label.EnumDeclaration; case SyntaxKind.DelegateDeclaration: return Label.DelegateDeclaration; case SyntaxKind.FieldDeclaration: case SyntaxKind.EventFieldDeclaration: return Label.FieldDeclaration; case SyntaxKind.ConversionOperatorDeclaration: return Label.ConversionOperatorDeclaration; case SyntaxKind.OperatorDeclaration: return Label.OperatorDeclaration; case SyntaxKind.DestructorDeclaration: isLeaf = true; return Label.DestructorDeclaration; case SyntaxKind.PropertyDeclaration: return Label.PropertyDeclaration; case SyntaxKind.IndexerDeclaration: return Label.IndexerDeclaration; case SyntaxKind.ArrowExpressionClause: if (node?.Parent is (kind: SyntaxKind.PropertyDeclaration or SyntaxKind.IndexerDeclaration)) return Label.ArrowExpressionClause; break; case SyntaxKind.EventDeclaration: return Label.EventDeclaration; case SyntaxKind.EnumMemberDeclaration: // not a leaf because an attribute may be applied return Label.EnumMemberDeclaration; case SyntaxKind.AccessorList: return Label.AccessorList; case SyntaxKind.GetAccessorDeclaration: case SyntaxKind.SetAccessorDeclaration: case SyntaxKind.InitAccessorDeclaration: case SyntaxKind.AddAccessorDeclaration: case SyntaxKind.RemoveAccessorDeclaration: isLeaf = true; return Label.AccessorDeclaration; // Note: These last two do actually appear as statement syntax, but mean something // different and hence have a different label case SyntaxKind.VariableDeclaration: return Label.FieldVariableDeclaration; case SyntaxKind.VariableDeclarator: // For top syntax, a variable declarator is a leaf node isLeaf = true; return Label.FieldVariableDeclarator; case SyntaxKind.AttributeList: // Only module/assembly attributes are labelled if (node is not null && node.IsParentKind(SyntaxKind.CompilationUnit)) { return Label.AttributeList; } break; case SyntaxKind.Attribute: // Only module/assembly attributes are labelled if (node is { Parent: { } parent } && parent.IsParentKind(SyntaxKind.CompilationUnit)) { isLeaf = true; return Label.Attribute; } break; case SyntaxKind.ExtensionBlockDeclaration: return Label.ExtensionBlockDeclaration; } // If we got this far, its an unlabelled node. For top // syntax, we don't need to descend into any ignored nodes isLeaf = true; return Label.Ignored; } // internal for testing internal bool HasLabel(SyntaxKind kind) => Classify(kind, node: null, out _) != Label.Ignored; protected internal override int LabelCount => (int)Label.Count; protected internal override int TiedToAncestor(int label) => TiedToAncestor((Label)label); #endregion #region Comparisons public override bool ValuesEqual(SyntaxNode left, SyntaxNode right) { Func<SyntaxKind, bool>? ignoreChildFunction; switch (left.Kind()) { // all syntax kinds with a method body child: case SyntaxKind.MethodDeclaration: case SyntaxKind.ConversionOperatorDeclaration: case SyntaxKind.OperatorDeclaration: case SyntaxKind.ConstructorDeclaration: case SyntaxKind.DestructorDeclaration: case SyntaxKind.GetAccessorDeclaration: case SyntaxKind.SetAccessorDeclaration: case SyntaxKind.InitAccessorDeclaration: case SyntaxKind.AddAccessorDeclaration: case SyntaxKind.RemoveAccessorDeclaration: // When comparing method bodies we need to NOT ignore VariableDeclaration and VariableDeclarator children, // but when comparing field definitions we should ignore VariableDeclarations children. var leftBody = GetBody(left); var rightBody = GetBody(right); if (!SyntaxFactory.AreEquivalent(leftBody, rightBody, null)) { return false; } ignoreChildFunction = childKind => childKind == SyntaxKind.Block || childKind == SyntaxKind.ArrowExpressionClause || HasLabel(childKind); break; case SyntaxKind.SwitchSection: return Equal((SwitchSectionSyntax)left, (SwitchSectionSyntax)right); case SyntaxKind.ForStatement: // The only children of ForStatement are labeled nodes and punctuation. return true; default: if (HasChildren(left)) { ignoreChildFunction = childKind => HasLabel(childKind); } else { ignoreChildFunction = null; } break; } return SyntaxFactory.AreEquivalent(left, right, ignoreChildFunction); } private bool Equal(SwitchSectionSyntax left, SwitchSectionSyntax right) { return SyntaxFactory.AreEquivalent(left.Labels, right.Labels, null) && SyntaxFactory.AreEquivalent(left.Statements, right.Statements, ignoreChildNode: HasLabel); } private static SyntaxNode? GetBody(SyntaxNode node) { switch (node) { case BaseMethodDeclarationSyntax baseMethodDeclarationSyntax: return baseMethodDeclarationSyntax.Body ?? (SyntaxNode?)baseMethodDeclarationSyntax.ExpressionBody?.Expression; case AccessorDeclarationSyntax accessorDeclarationSyntax: return accessorDeclarationSyntax.Body ?? (SyntaxNode?)accessorDeclarationSyntax.ExpressionBody?.Expression; default: throw ExceptionUtilities.UnexpectedValue(node); } } protected override bool TryComputeWeightedDistance(SyntaxNode leftNode, SyntaxNode rightNode, out double distance) { switch (leftNode.Kind()) { case SyntaxKind.VariableDeclarator: distance = ComputeDistance( ((VariableDeclaratorSyntax)leftNode).Identifier, ((VariableDeclaratorSyntax)rightNode).Identifier); return true; case SyntaxKind.ForStatement: var leftFor = (ForStatementSyntax)leftNode; var rightFor = (ForStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftFor, rightFor); return true; case SyntaxKind.ForEachStatement: case SyntaxKind.ForEachVariableStatement: { var leftForEach = (CommonForEachStatementSyntax)leftNode; var rightForEach = (CommonForEachStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftForEach, rightForEach); return true; } case SyntaxKind.UsingStatement: { var leftUsing = (UsingStatementSyntax)leftNode; var rightUsing = (UsingStatementSyntax)rightNode; if (leftUsing.Declaration != null && rightUsing.Declaration != null) { distance = ComputeWeightedDistance( leftUsing.Declaration, leftUsing.Statement, rightUsing.Declaration, rightUsing.Statement); } else { distance = ComputeWeightedDistance( (SyntaxNode?)leftUsing.Expression ?? leftUsing.Declaration!, leftUsing.Statement, (SyntaxNode?)rightUsing.Expression ?? rightUsing.Declaration!, rightUsing.Statement); } return true; } case SyntaxKind.UsingDirective: { var leftUsing = (UsingDirectiveSyntax)leftNode; var rightUsing = (UsingDirectiveSyntax)rightNode; // For now, just compute the distances of both the alias and name and combine their weights // 50/50. We could consider weighting the alias more heavily. i.e. if you have `using X = ...` // and `using X = ...` it's more likely that this is the same alias, and just the name portion // changed versus thinking that some other using became this alias. distance = ComputeDistance(leftUsing.Alias, rightUsing.Alias) + ComputeDistance(leftUsing.NamespaceOrType, rightUsing.NamespaceOrType); // Consider two usings that only differ by presence/absence of 'global' to be a near match. if (leftUsing.GlobalKeyword.IsKind(SyntaxKind.None) != rightUsing.GlobalKeyword.IsKind(SyntaxKind.None)) distance += EpsilonDist; // Consider two usings that only differ by presence/absence of 'unsafe' to be a near match. if (leftUsing.UnsafeKeyword.IsKind(SyntaxKind.None) != rightUsing.UnsafeKeyword.IsKind(SyntaxKind.None)) distance += EpsilonDist; return true; } case SyntaxKind.LockStatement: var leftLock = (LockStatementSyntax)leftNode; var rightLock = (LockStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftLock.Expression, leftLock.Statement, rightLock.Expression, rightLock.Statement); return true; case SyntaxKind.FixedStatement: var leftFixed = (FixedStatementSyntax)leftNode; var rightFixed = (FixedStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftFixed.Declaration, leftFixed.Statement, rightFixed.Declaration, rightFixed.Statement); return true; case SyntaxKind.WhileStatement: var leftWhile = (WhileStatementSyntax)leftNode; var rightWhile = (WhileStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftWhile.Condition, leftWhile.Statement, rightWhile.Condition, rightWhile.Statement); return true; case SyntaxKind.DoStatement: var leftDo = (DoStatementSyntax)leftNode; var rightDo = (DoStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftDo.Condition, leftDo.Statement, rightDo.Condition, rightDo.Statement); return true; case SyntaxKind.IfStatement: var leftIf = (IfStatementSyntax)leftNode; var rightIf = (IfStatementSyntax)rightNode; distance = ComputeWeightedDistance(leftIf.Condition, leftIf.Statement, rightIf.Condition, rightIf.Statement); return true; case SyntaxKind.Block: var leftBlock = (BlockSyntax)leftNode; var rightBlock = (BlockSyntax)rightNode; return TryComputeWeightedDistance(leftBlock, rightBlock, out distance); case SyntaxKind.CatchClause: distance = ComputeWeightedDistance((CatchClauseSyntax)leftNode, (CatchClauseSyntax)rightNode); return true; case SyntaxKind.ParenthesizedLambdaExpression: case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.AnonymousMethodExpression: case SyntaxKind.LocalFunctionStatement: distance = ComputeWeightedDistanceOfNestedFunctions(leftNode, rightNode); return true; case SyntaxKind.SingleVariableDesignation: distance = ComputeWeightedDistance((SingleVariableDesignationSyntax)leftNode, (SingleVariableDesignationSyntax)rightNode); return true; case SyntaxKind.TypeParameterConstraintClause: distance = ComputeDistance((TypeParameterConstraintClauseSyntax)leftNode, (TypeParameterConstraintClauseSyntax)rightNode); return true; case SyntaxKind.TypeParameter: distance = ComputeDistance((TypeParameterSyntax)leftNode, (TypeParameterSyntax)rightNode); return true; case SyntaxKind.Parameter: distance = ComputeDistance((ParameterSyntax)leftNode, (ParameterSyntax)rightNode); return true; case SyntaxKind.AttributeList: distance = ComputeDistance((AttributeListSyntax)leftNode, (AttributeListSyntax)rightNode); return true; case SyntaxKind.Attribute: distance = ComputeDistance((AttributeSyntax)leftNode, (AttributeSyntax)rightNode); return true; case SyntaxKind.ExtensionBlockDeclaration: distance = ComputeDistance((ExtensionBlockDeclarationSyntax)leftNode, (ExtensionBlockDeclarationSyntax)rightNode); return true; default: var leftName = TryGetName(leftNode); var rightName = TryGetName(rightNode); Contract.ThrowIfFalse(rightName.HasValue == leftName.HasValue); if (leftName.HasValue) { distance = ComputeDistance(leftName.Value, rightName!.Value); return true; } else { distance = 0; return false; } } } private static double ComputeWeightedDistanceOfNestedFunctions(SyntaxNode leftNode, SyntaxNode rightNode) { GetNestedFunctionsParts(leftNode, out var leftParameters, out var leftAsync, out var leftBody, out var leftModifiers, out var leftReturnType, out var leftIdentifier, out var leftTypeParameters); GetNestedFunctionsParts(rightNode, out var rightParameters, out var rightAsync, out var rightBody, out var rightModifiers, out var rightReturnType, out var rightIdentifier, out var rightTypeParameters); if ((leftAsync.Kind() == SyntaxKind.AsyncKeyword) != (rightAsync.Kind() == SyntaxKind.AsyncKeyword)) { return 1.0; } var modifierDistance = ComputeDistance(leftModifiers, rightModifiers); var returnTypeDistance = ComputeDistance(leftReturnType, rightReturnType); var identifierDistance = ComputeDistance(leftIdentifier, rightIdentifier); var typeParameterDistance = ComputeDistance(leftTypeParameters, rightTypeParameters); var parameterDistance = ComputeDistance(leftParameters, rightParameters); var bodyDistance = ComputeDistance(leftBody, rightBody); return modifierDistance * 0.1 + returnTypeDistance * 0.1 + identifierDistance * 0.2 + typeParameterDistance * 0.2 + parameterDistance * 0.2 + bodyDistance * 0.2; } private static void GetNestedFunctionsParts( SyntaxNode nestedFunction, out IEnumerable<SyntaxToken> parameters, out SyntaxToken asyncKeyword, out SyntaxNode body, out SyntaxTokenList modifiers, out TypeSyntax? returnType, out SyntaxToken identifier, out TypeParameterListSyntax? typeParameters) { switch (nestedFunction.Kind()) { case SyntaxKind.SimpleLambdaExpression: var simple = (SimpleLambdaExpressionSyntax)nestedFunction; parameters = simple.Parameter.DescendantTokens(); asyncKeyword = simple.AsyncKeyword; body = simple.Body; modifiers = default; returnType = null; identifier = default; typeParameters = null; break; case SyntaxKind.ParenthesizedLambdaExpression: var parenthesized = (ParenthesizedLambdaExpressionSyntax)nestedFunction; parameters = GetDescendantTokensIgnoringSeparators(parenthesized.ParameterList.Parameters); asyncKeyword = parenthesized.AsyncKeyword; body = parenthesized.Body; modifiers = default; returnType = null; identifier = default; typeParameters = null; break; case SyntaxKind.AnonymousMethodExpression: var anonymous = (AnonymousMethodExpressionSyntax)nestedFunction; if (anonymous.ParameterList != null) { parameters = GetDescendantTokensIgnoringSeparators(anonymous.ParameterList.Parameters); } else { parameters = []; } asyncKeyword = anonymous.AsyncKeyword; body = anonymous.Block; modifiers = default; returnType = null; identifier = default; typeParameters = null; break; case SyntaxKind.LocalFunctionStatement: var localFunction = (LocalFunctionStatementSyntax)nestedFunction; parameters = GetDescendantTokensIgnoringSeparators(localFunction.ParameterList.Parameters); asyncKeyword = default; body = (SyntaxNode?)localFunction.Body ?? localFunction.ExpressionBody!; modifiers = localFunction.Modifiers; returnType = localFunction.ReturnType; identifier = localFunction.Identifier; typeParameters = localFunction.TypeParameterList; break; default: throw ExceptionUtilities.UnexpectedValue(nestedFunction.Kind()); } } private bool TryComputeWeightedDistance(BlockSyntax leftBlock, BlockSyntax rightBlock, out double distance) { // No block can be matched with the root block. // Note that in constructors the root is the constructor declaration, since we need to include // the constructor initializer in the match. if (leftBlock.Parent == null || rightBlock.Parent == null || leftBlock.Parent.IsKind(SyntaxKind.ConstructorDeclaration) || rightBlock.Parent.IsKind(SyntaxKind.ConstructorDeclaration)) { distance = 0.0; return true; } if (GetLabel(leftBlock.Parent) != GetLabel(rightBlock.Parent)) { distance = 0.2 + 0.8 * ComputeWeightedBlockDistance(leftBlock, rightBlock); return true; } switch (leftBlock.Parent.Kind()) { case SyntaxKind.IfStatement: case SyntaxKind.ForEachStatement: case SyntaxKind.ForEachVariableStatement: case SyntaxKind.ForStatement: case SyntaxKind.WhileStatement: case SyntaxKind.DoStatement: case SyntaxKind.FixedStatement: case SyntaxKind.LockStatement: case SyntaxKind.UsingStatement: case SyntaxKind.SwitchSection: case SyntaxKind.ParenthesizedLambdaExpression: case SyntaxKind.SimpleLambdaExpression: case SyntaxKind.AnonymousMethodExpression: case SyntaxKind.LocalFunctionStatement: // value distance of the block body is included: distance = GetDistance(leftBlock.Parent, rightBlock.Parent); return true; case SyntaxKind.CatchClause: var leftCatch = (CatchClauseSyntax)leftBlock.Parent; var rightCatch = (CatchClauseSyntax)rightBlock.Parent; if (leftCatch is { Declaration: null, Filter: null } && rightCatch is { Declaration: null, Filter: null }) { var leftTry = (TryStatementSyntax)leftCatch.Parent!; var rightTry = (TryStatementSyntax)rightCatch.Parent!; distance = 0.5 * ComputeValueDistance(leftTry.Block, rightTry.Block) + 0.5 * ComputeValueDistance(leftBlock, rightBlock); } else { // value distance of the block body is included: distance = GetDistance(leftBlock.Parent, rightBlock.Parent); } return true; case SyntaxKind.Block: case SyntaxKind.LabeledStatement: case SyntaxKind.GlobalStatement: distance = ComputeWeightedBlockDistance(leftBlock, rightBlock); return true; case SyntaxKind.UnsafeStatement: case SyntaxKind.CheckedStatement: case SyntaxKind.UncheckedStatement: case SyntaxKind.ElseClause: case SyntaxKind.FinallyClause: case SyntaxKind.TryStatement: distance = 0.2 * ComputeValueDistance(leftBlock, rightBlock); return true; default: throw ExceptionUtilities.UnexpectedValue(leftBlock.Parent.Kind()); } } private double ComputeWeightedDistance(SingleVariableDesignationSyntax leftNode, SingleVariableDesignationSyntax rightNode) { var distance = ComputeDistance(leftNode, rightNode); double parentDistance; if (leftNode.Parent != null && rightNode.Parent != null && GetLabel(leftNode.Parent) == GetLabel(rightNode.Parent)) { parentDistance = ComputeDistance(leftNode.Parent, rightNode.Parent); } else { parentDistance = 1; } return 0.5 * parentDistance + 0.5 * distance; } private static double ComputeWeightedBlockDistance(BlockSyntax leftBlock, BlockSyntax rightBlock) { if (TryComputeLocalsDistance(leftBlock, rightBlock, out var distance)) { return distance; } return ComputeValueDistance(leftBlock, rightBlock); } private static double ComputeWeightedDistance(CatchClauseSyntax left, CatchClauseSyntax right) { var blockDistance = ComputeDistance(left.Block, right.Block); var distance = CombineOptional(blockDistance, left.Declaration, right.Declaration, left.Filter, right.Filter); return AdjustForLocalsInBlock(distance, left.Block, right.Block, localsWeight: 0.3); } private static double ComputeWeightedDistance( CommonForEachStatementSyntax leftCommonForEach, CommonForEachStatementSyntax rightCommonForEach) { var statementDistance = ComputeDistance(leftCommonForEach.Statement, rightCommonForEach.Statement); var expressionDistance = ComputeDistance(leftCommonForEach.Expression, rightCommonForEach.Expression); List<SyntaxToken>? leftLocals = null; List<SyntaxToken>? rightLocals = null; GetLocalNames(leftCommonForEach, ref leftLocals); GetLocalNames(rightCommonForEach, ref rightLocals); var localNamesDistance = ComputeDistance(leftLocals, rightLocals); var distance = localNamesDistance * 0.6 + expressionDistance * 0.2 + statementDistance * 0.2; return AdjustForLocalsInBlock(distance, leftCommonForEach.Statement, rightCommonForEach.Statement, localsWeight: 0.6); } private static double ComputeWeightedDistance(ForStatementSyntax left, ForStatementSyntax right) { var statementDistance = ComputeDistance(left.Statement, right.Statement); var conditionDistance = ComputeDistance(left.Condition, right.Condition); var incDistance = ComputeDistance( GetDescendantTokensIgnoringSeparators(left.Incrementors), GetDescendantTokensIgnoringSeparators(right.Incrementors)); var distance = conditionDistance * 0.3 + incDistance * 0.3 + statementDistance * 0.4; if (TryComputeLocalsDistance(left.Declaration, right.Declaration, out var localsDistance)) { distance = distance * 0.4 + localsDistance * 0.6; } return distance; } private static double ComputeWeightedDistance( VariableDeclarationSyntax leftVariables, StatementSyntax leftStatement, VariableDeclarationSyntax rightVariables, StatementSyntax rightStatement) { var distance = ComputeDistance(leftStatement, rightStatement); // Put maximum weight behind the variables declared in the header of the statement. if (TryComputeLocalsDistance(leftVariables, rightVariables, out var localsDistance)) { distance = distance * 0.4 + localsDistance * 0.6; } // If the statement is a block that declares local variables, // weight them more than the rest of the statement. return AdjustForLocalsInBlock(distance, leftStatement, rightStatement, localsWeight: 0.2); } private static double ComputeWeightedDistance( SyntaxNode? leftHeader, StatementSyntax leftStatement, SyntaxNode? rightHeader, StatementSyntax rightStatement) { var headerDistance = ComputeDistance(leftHeader, rightHeader); var statementDistance = ComputeDistance(leftStatement, rightStatement); var distance = headerDistance * 0.6 + statementDistance * 0.4; return AdjustForLocalsInBlock(distance, leftStatement, rightStatement, localsWeight: 0.5); } private static double AdjustForLocalsInBlock( double distance, StatementSyntax leftStatement, StatementSyntax rightStatement, double localsWeight) { // If the statement is a block that declares local variables, // weight them more than the rest of the statement. if (leftStatement.Kind() == SyntaxKind.Block && rightStatement.Kind() == SyntaxKind.Block) { if (TryComputeLocalsDistance((BlockSyntax)leftStatement, (BlockSyntax)rightStatement, out var localsDistance)) { return localsDistance * localsWeight + distance * (1 - localsWeight); } } return distance; } private static bool TryComputeLocalsDistance(VariableDeclarationSyntax? left, VariableDeclarationSyntax? right, out double distance) { List<SyntaxToken>? leftLocals = null; List<SyntaxToken>? rightLocals = null; if (left != null) { GetLocalNames(left, ref leftLocals); } if (right != null) { GetLocalNames(right, ref rightLocals); } if (leftLocals == null || rightLocals == null) { distance = 0; return false; } distance = ComputeDistance(leftLocals, rightLocals); return true; } private static bool TryComputeLocalsDistance(BlockSyntax left, BlockSyntax right, out double distance) { List<SyntaxToken>? leftLocals = null; List<SyntaxToken>? rightLocals = null; GetLocalNames(left, ref leftLocals); GetLocalNames(right, ref rightLocals); if (leftLocals == null || rightLocals == null) { distance = 0; return false; } distance = ComputeDistance(leftLocals, rightLocals); return true; } // Doesn't include variables declared in declaration expressions // Consider including them (https://github.com/dotnet/roslyn/issues/37460). private static void GetLocalNames(BlockSyntax block, ref List<SyntaxToken>? result) { foreach (var child in block.ChildNodes()) { if (child is LocalDeclarationStatementSyntax localDecl) { GetLocalNames(localDecl.Declaration, ref result); } } } // Doesn't include variables declared in declaration expressions // Consider including them (https://github.com/dotnet/roslyn/issues/37460). private static void GetLocalNames(VariableDeclarationSyntax localDeclaration, ref List<SyntaxToken>? result) { foreach (var local in localDeclaration.Variables) { GetLocalNames(local.Identifier, ref result); } } internal static void GetLocalNames(CommonForEachStatementSyntax commonForEach, ref List<SyntaxToken>? result) { switch (commonForEach.Kind()) { case SyntaxKind.ForEachStatement: GetLocalNames(((ForEachStatementSyntax)commonForEach).Identifier, ref result); return; case SyntaxKind.ForEachVariableStatement: var forEachVariable = (ForEachVariableStatementSyntax)commonForEach; GetLocalNames(forEachVariable.Variable, ref result); return; default: throw ExceptionUtilities.UnexpectedValue(commonForEach.Kind()); } } private static void GetLocalNames(ExpressionSyntax expression, ref List<SyntaxToken>? result) { switch (expression.Kind()) { case SyntaxKind.DeclarationExpression: var declarationExpression = (DeclarationExpressionSyntax)expression; var localDeclaration = declarationExpression.Designation; GetLocalNames(localDeclaration, ref result); return; case SyntaxKind.TupleExpression: var tupleExpression = (TupleExpressionSyntax)expression; foreach (var argument in tupleExpression.Arguments) { GetLocalNames(argument.Expression, ref result); } return; default: // Do nothing for node that cannot have variable declarations inside. return; } } private static void GetLocalNames(VariableDesignationSyntax designation, ref List<SyntaxToken>? result) { switch (designation.Kind()) { case SyntaxKind.SingleVariableDesignation: GetLocalNames(((SingleVariableDesignationSyntax)designation).Identifier, ref result); return; case SyntaxKind.ParenthesizedVariableDesignation: var parenthesizedVariableDesignation = (ParenthesizedVariableDesignationSyntax)designation; foreach (var variableDesignation in parenthesizedVariableDesignation.Variables) { GetLocalNames(variableDesignation, ref result); } return; case SyntaxKind.DiscardDesignation: return; default: throw ExceptionUtilities.UnexpectedValue(designation.Kind()); } } private static void GetLocalNames(SyntaxToken syntaxToken, [NotNull] ref List<SyntaxToken>? result) { result ??= []; result.Add(syntaxToken); } private static double CombineOptional( double distance0, SyntaxNode? left1, SyntaxNode? right1, SyntaxNode? left2, SyntaxNode? right2, double weight0 = 0.8, double weight1 = 0.5) { var one = left1 != null || right1 != null; var two = left2 != null || right2 != null; if (!one && !two) { return distance0; } var distance1 = ComputeDistance(left1, right1); var distance2 = ComputeDistance(left2, right2); double d; if (one && two) { d = distance1 * weight1 + distance2 * (1 - weight1); } else if (one) { d = distance1; } else { d = distance2; } return distance0 * weight0 + d * (1 - weight0); } private static SyntaxNodeOrToken? TryGetName(SyntaxNode node) { switch (node.Kind()) { case SyntaxKind.ExternAliasDirective: return ((ExternAliasDirectiveSyntax)node).Identifier; case SyntaxKind.UsingDirective: return ((UsingDirectiveSyntax)node).NamespaceOrType; case SyntaxKind.NamespaceDeclaration: case SyntaxKind.FileScopedNamespaceDeclaration: return ((BaseNamespaceDeclarationSyntax)node).Name; case SyntaxKind.ClassDeclaration: case SyntaxKind.StructDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.RecordDeclaration: case SyntaxKind.RecordStructDeclaration: return ((TypeDeclarationSyntax)node).Identifier; case SyntaxKind.EnumDeclaration: return ((EnumDeclarationSyntax)node).Identifier; case SyntaxKind.DelegateDeclaration: return ((DelegateDeclarationSyntax)node).Identifier; case SyntaxKind.ExtensionBlockDeclaration: return null; case SyntaxKind.FieldDeclaration: case SyntaxKind.EventFieldDeclaration: case SyntaxKind.VariableDeclaration: return null; case SyntaxKind.VariableDeclarator: return ((VariableDeclaratorSyntax)node).Identifier; case SyntaxKind.MethodDeclaration: return ((MethodDeclarationSyntax)node).Identifier; case SyntaxKind.ConversionOperatorDeclaration: return ((ConversionOperatorDeclarationSyntax)node).Type; case SyntaxKind.OperatorDeclaration: return ((OperatorDeclarationSyntax)node).OperatorToken; case SyntaxKind.ConstructorDeclaration: return ((ConstructorDeclarationSyntax)node).Identifier; case SyntaxKind.DestructorDeclaration: return ((DestructorDeclarationSyntax)node).Identifier; case SyntaxKind.PropertyDeclaration: return ((PropertyDeclarationSyntax)node).Identifier; case SyntaxKind.IndexerDeclaration: return null; case SyntaxKind.ArrowExpressionClause: return null; case SyntaxKind.EventDeclaration: return ((EventDeclarationSyntax)node).Identifier; case SyntaxKind.EnumMemberDeclaration: return ((EnumMemberDeclarationSyntax)node).Identifier; case SyntaxKind.GetAccessorDeclaration: case SyntaxKind.SetAccessorDeclaration: case SyntaxKind.InitAccessorDeclaration: return null; case SyntaxKind.TypeParameterConstraintClause: return ((TypeParameterConstraintClauseSyntax)node).Name.Identifier; case SyntaxKind.TypeParameter: return ((TypeParameterSyntax)node).Identifier; case SyntaxKind.TypeParameterList: case SyntaxKind.ParameterList: case SyntaxKind.BracketedParameterList: return null; case SyntaxKind.Parameter: return ((ParameterSyntax)node).Identifier; case SyntaxKind.AttributeList: return ((AttributeListSyntax)node).Target; case SyntaxKind.Attribute: return ((AttributeSyntax)node).Name; default: return null; } } public sealed override double GetDistance(SyntaxNode oldNode, SyntaxNode newNode) { Debug.Assert(GetLabel(oldNode) == GetLabel(newNode) && GetLabel(oldNode) != IgnoredNode); if (oldNode == newNode) { return ExactMatchDist; } if (TryComputeWeightedDistance(oldNode, newNode, out var weightedDistance)) { if (weightedDistance == ExactMatchDist && !SyntaxFactory.AreEquivalent(oldNode, newNode)) { weightedDistance = EpsilonDist; } return weightedDistance; } return ComputeValueDistance(oldNode, newNode); } internal static double ComputeValueDistance(SyntaxNode? oldNode, SyntaxNode? newNode) { if (SyntaxFactory.AreEquivalent(oldNode, newNode)) { return ExactMatchDist; } var distance = ComputeDistance(oldNode, newNode); // We don't want to return an exact match, because there // must be something different, since we got here return (distance == ExactMatchDist) ? EpsilonDist : distance; } internal static double ComputeDistance(SyntaxNodeOrToken oldNodeOrToken, SyntaxNodeOrToken newNodeOrToken) { Debug.Assert(newNodeOrToken.IsToken == oldNodeOrToken.IsToken); double distance; if (oldNodeOrToken.IsToken) { var leftToken = oldNodeOrToken.AsToken(); var rightToken = newNodeOrToken.AsToken(); distance = ComputeDistance(leftToken, rightToken); Debug.Assert(!SyntaxFactory.AreEquivalent(leftToken, rightToken) || distance == ExactMatchDist); } else { var leftNode = oldNodeOrToken.AsNode(); var rightNode = newNodeOrToken.AsNode(); distance = ComputeDistance(leftNode, rightNode); Debug.Assert(!SyntaxFactory.AreEquivalent(leftNode, rightNode) || distance == ExactMatchDist); } return distance; } /// <summary> /// Enumerates tokens of all nodes in the list. Doesn't include separators. /// </summary> internal static IEnumerable<SyntaxToken> GetDescendantTokensIgnoringSeparators<TSyntaxNode>(SeparatedSyntaxList<TSyntaxNode> list) where TSyntaxNode : SyntaxNode { foreach (var node in list) { foreach (var token in node.DescendantTokens()) { yield return token; } } } /// <summary> /// Calculates the distance between two syntax nodes, disregarding trivia. /// </summary> /// <remarks> /// Distance is a number within [0, 1], the smaller the more similar the nodes are. /// </remarks> public static double ComputeDistance(SyntaxNode? oldNode, SyntaxNode? newNode) { if (oldNode == null || newNode == null) { return (oldNode == newNode) ? 0.0 : 1.0; } return ComputeDistance(oldNode.DescendantTokens(), newNode.DescendantTokens()); } /// <summary> /// Calculates the distance between two syntax tokens, disregarding trivia. /// </summary> /// <remarks> /// Distance is a number within [0, 1], the smaller the more similar the tokens are. /// </remarks> public static double ComputeDistance(SyntaxToken oldToken, SyntaxToken newToken) => LongestCommonSubstring.ComputePrefixDistance( oldToken.Text, Math.Min(oldToken.Text.Length, LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation), newToken.Text, Math.Min(newToken.Text.Length, LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation)); private static ImmutableArray<T> CreateArrayForDistanceCalculation<T>(IEnumerable<T>? enumerable) => enumerable is null ? [] : [.. enumerable.Take(LongestCommonSubsequence.MaxSequenceLengthForDistanceCalculation)]; /// <summary> /// Calculates the distance between two sequences of syntax tokens, disregarding trivia. /// </summary> /// <remarks> /// Distance is a number within [0, 1], the smaller the more similar the sequences are. /// </remarks> public static double ComputeDistance(IEnumerable<SyntaxToken>? oldTokens, IEnumerable<SyntaxToken>? newTokens) => LcsTokens.Instance.ComputeDistance(CreateArrayForDistanceCalculation(oldTokens), CreateArrayForDistanceCalculation(newTokens)); /// <summary> /// Calculates the distance between two sequences of syntax nodes, disregarding trivia. /// </summary> /// <remarks> /// Distance is a number within [0, 1], the smaller the more similar the sequences are. /// </remarks> public static double ComputeDistance(IEnumerable<SyntaxNode>? oldNodes, IEnumerable<SyntaxNode>? newNodes) => LcsNodes.Instance.ComputeDistance(CreateArrayForDistanceCalculation(oldNodes), CreateArrayForDistanceCalculation(newNodes)); /// <summary> /// Calculates the edits that transform one sequence of syntax nodes to another, disregarding trivia. /// </summary> public static IEnumerable<SequenceEdit> GetSequenceEdits(IEnumerable<SyntaxNode>? oldNodes, IEnumerable<SyntaxNode>? newNodes) => LcsNodes.Instance.GetEdits(oldNodes.AsImmutableOrEmpty(), newNodes.AsImmutableOrEmpty()); /// <summary> /// Calculates the edits that transform one sequence of syntax nodes to another, disregarding trivia. /// </summary> public static IEnumerable<SequenceEdit> GetSequenceEdits(ImmutableArray<SyntaxNode> oldNodes, ImmutableArray<SyntaxNode> newNodes) => LcsNodes.Instance.GetEdits(oldNodes.NullToEmpty(), newNodes.NullToEmpty()); /// <summary> /// Calculates the edits that transform one sequence of syntax tokens to another, disregarding trivia. /// </summary> public static IEnumerable<SequenceEdit> GetSequenceEdits(IEnumerable<SyntaxToken>? oldTokens, IEnumerable<SyntaxToken>? newTokens) => LcsTokens.Instance.GetEdits(oldTokens.AsImmutableOrEmpty(), newTokens.AsImmutableOrEmpty()); /// <summary> /// Calculates the edits that transform one sequence of syntax tokens to another, disregarding trivia. /// </summary> public static IEnumerable<SequenceEdit> GetSequenceEdits(ImmutableArray<SyntaxToken> oldTokens, ImmutableArray<SyntaxToken> newTokens) => LcsTokens.Instance.GetEdits(oldTokens.NullToEmpty(), newTokens.NullToEmpty()); private sealed class LcsTokens : LongestCommonImmutableArraySubsequence<SyntaxToken> { internal static readonly LcsTokens Instance = new(); protected override bool Equals(SyntaxToken oldElement, SyntaxToken newElement) => SyntaxFactory.AreEquivalent(oldElement, newElement); } private sealed class LcsNodes : LongestCommonImmutableArraySubsequence<SyntaxNode> { internal static readonly LcsNodes Instance = new(); protected override bool Equals(SyntaxNode oldElement, SyntaxNode newElement) => SyntaxFactory.AreEquivalent(oldElement, newElement); } #endregion }