File: src\Workspaces\SharedUtilitiesAndExtensions\Compiler\Core\Services\RefactoringHelpers\AbstractRefactoringHelpers.cs
Web Access
Project: src\src\RoslynAnalyzers\Tools\GenerateDocumentationAndConfigFiles\GenerateDocumentationAndConfigFiles.csproj (GenerateDocumentationAndConfigFiles)
// 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.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.CodeRefactorings;
 
internal abstract class AbstractRefactoringHelpers<TExpressionSyntax, TArgumentSyntax, TExpressionStatementSyntax> : IRefactoringHelpers
    where TExpressionSyntax : SyntaxNode
    where TArgumentSyntax : SyntaxNode
    where TExpressionStatementSyntax : SyntaxNode
{
    protected abstract ISyntaxFacts SyntaxFacts { get; }
    protected abstract IHeaderFacts HeaderFacts { get; }
 
    public abstract bool IsBetweenTypeMembers(SourceText sourceText, SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? typeDeclaration);
 
    private static void AddNode<TSyntaxNode>(bool allowEmptyNodes, ref TemporaryArray<TSyntaxNode> result, TSyntaxNode node) where TSyntaxNode : SyntaxNode
    {
        if (!allowEmptyNodes && node.Span.IsEmpty)
            return;
 
        result.Add(node);
    }
 
    /// <summary>
    /// Trims leading and trailing whitespace from <paramref name="span"/>.
    /// </summary>
    /// <remarks>
    /// Returns unchanged <paramref name="span"/> in case <see cref="TextSpan.IsEmpty"/>.
    /// Returns empty Span with original <see cref="TextSpan.Start"/> in case it contains only whitespace.
    /// </remarks>
    private static TextSpan GetTrimmedTextSpan(SourceText sourceText, TextSpan span)
    {
        if (span.IsEmpty)
            return span;
 
        var start = span.Start;
        var end = span.End;
 
        while (start < end && char.IsWhiteSpace(sourceText[end - 1]))
            end--;
 
        while (start < end && char.IsWhiteSpace(sourceText[start]))
            start++;
 
        return TextSpan.FromBounds(start, end);
    }
 
    public void AddRelevantNodes<TSyntaxNode>(
        SourceText sourceText, SyntaxNode root, TextSpan selectionRaw, bool allowEmptyNodes, int maxCount, ref TemporaryArray<TSyntaxNode> result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        // Given selection is trimmed first to enable over-selection that spans multiple lines. Since trailing whitespace ends
        // at newline boundary over-selection to e.g. a line after LocalFunctionStatement would cause FindNode to find enclosing
        // block's Node. That is because in addition to LocalFunctionStatement the selection would also contain trailing trivia 
        // (whitespace) of following statement.
 
        var syntaxFacts = this.SyntaxFacts;
        var headerFacts = this.HeaderFacts;
        var selectionTrimmed = GetTrimmedTextSpan(sourceText, selectionRaw);
 
        // If user selected only whitespace we don't want to return anything. We could do following:
        //  1) Consider token that owns (as its trivia) the whitespace.
        //  2) Consider start/beginning of whitespace as location (empty selection)
        // Option 1) can't be used all the time and 2) can be confusing for users. Therefore bailing out is the
        // most consistent option.
        if (selectionTrimmed.IsEmpty && !selectionRaw.IsEmpty)
            return;
 
        // Every time a Node is considered an extractNodes method is called to add all nodes around the original one
        // that should also be considered.
        //
        // That enables us to e.g. return node `b` when Node `var a = b;` is being considered without a complex (and potentially 
        // lang. & situation dependent) into Children descending code here.  We can't just try extracted Node because we might 
        // want the whole node `var a = b;`
 
        // Handle selections:
        // - Most/the whole wanted Node is selected (e.g. `C [|Fun() {}|]`
        //   - The smallest node whose FullSpan includes the whole (trimmed) selection
        //   - Using FullSpan is important because it handles over-selection with comments
        //   - Travels upwards through same-sized (FullSpan) nodes, extracting
        // - Token with wanted Node as direct parent is selected (e.g. IdentifierToken for LocalFunctionStatement: `C [|Fun|]() {}`) 
        // Note: Whether we have selection or location has to be checked against original selection because selecting just
        // whitespace could collapse selectionTrimmed into and empty Location. But we don't want `[|   |]token`
        // registering as `   [||]token`.
        if (!selectionTrimmed.IsEmpty)
        {
            AddRelevantNodesForSelection(syntaxFacts, root, selectionTrimmed, allowEmptyNodes, maxCount, ref result, cancellationToken);
        }
        else
        {
            var location = selectionTrimmed.Start;
 
            // No more selection -> Handle what current selection is touching:
            //
            // Consider touching only for empty selections. Otherwise `[|C|] methodName(){}` would be considered as
            // touching the Method's Node (through the left edge, see below) which is something the user probably
            // didn't want since they specifically selected only the return type.
            //
            // What the selection is touching is used in two ways. 
            // - Firstly, it is used to handle situation where it touches a Token whose direct ancestor is wanted
            //   Node. While having the (even empty) selection inside such token or to left of such Token is already
            //   handle by code above touching it from right `C methodName[||](){}` isn't (the FindNode for that
            //   returns Args node).
            // 
            // - Secondly, it is used for left/right edge climbing. E.g. `[||]C methodName(){}` the touching token's
            //   direct ancestor is TypeNode for the return type but it is still reasonable to expect that the user
            //   might want to be given refactorings for the whole method (as he has caret on the edge of it).
            //   Therefore we travel the Node tree upwards and as long as we're on the left edge of a Node's span we
            //   consider such node & potentially continue traveling upwards. The situation for right edge (`C
            //   methodName(){}[||]`) is analogical. E.g. for right edge `C methodName(){}[||]`: CloseBraceToken ->
            //   BlockSyntax -> LocalFunctionStatement -> null (higher node doesn't end on position anymore) Note:
            //   left-edge climbing needs to handle AttributeLists explicitly, see below for more information. 
            //
            // - Thirdly, if location isn't touching anything, we move the location to the token in whose trivia
            //   location is in. more about that below.
            // 
            // - Fourthly, if we're in an expression / argument we consider touching a parent expression whenever
            //   we're within it as long as it is on the first line of such expression (arbitrary heuristic).
 
            // In addition to per-node extr also check if current location (if selection is empty) is in a header of
            // higher level desired node once. We do that only for locations because otherwise `[|int|] A { get;
            // set; }) would trigger all refactorings for Property Decl. We cannot check this any sooner because the
            // above code could've changed current location.
            AddNonHiddenCorrectTypeNodes(ExtractNodesInHeader(root, location, headerFacts), allowEmptyNodes, maxCount, ref result, cancellationToken);
            if (result.Count >= maxCount)
                return;
 
            var (tokenToLeft, tokenToRight) = GetTokensToLeftAndRight(sourceText, root, location);
 
            // Add Nodes for touching tokens as described above.
            AddNodesForTokenToRight(syntaxFacts, root, allowEmptyNodes, maxCount, ref result, tokenToRight, cancellationToken);
            if (result.Count >= maxCount)
                return;
 
            AddNodesForTokenToLeft(syntaxFacts, allowEmptyNodes, maxCount, ref result, tokenToLeft, cancellationToken);
            if (result.Count >= maxCount)
                return;
 
            // If the wanted node is an expression syntax -> traverse upwards even if location is deep within a SyntaxNode.
            // We want to treat more types like expressions, e.g.: ArgumentSyntax should still trigger even if deep-in.
            if (IsWantedTypeExpressionLike<TSyntaxNode>())
            {
                // Reason to treat Arguments (and potentially others) as Expression-like: 
                // https://github.com/dotnet/roslyn/pull/37295#issuecomment-516145904
                AddNodesDeepIn(sourceText, root, location, allowEmptyNodes, maxCount, ref result, cancellationToken);
            }
        }
    }
 
    private static bool IsWantedTypeExpressionLike<TSyntaxNode>() where TSyntaxNode : SyntaxNode
    {
        var wantedType = typeof(TSyntaxNode);
 
        var expressionType = typeof(TExpressionSyntax);
        var argumentType = typeof(TArgumentSyntax);
        var expressionStatementType = typeof(TExpressionStatementSyntax);
 
        return IsAEqualOrSubclassOfB(wantedType, expressionType) ||
            IsAEqualOrSubclassOfB(wantedType, argumentType) ||
            IsAEqualOrSubclassOfB(wantedType, expressionStatementType);
 
        static bool IsAEqualOrSubclassOfB(Type a, Type b)
        {
            return a == b || a.IsSubclassOf(b);
        }
    }
 
    private (SyntaxToken tokenToLeft, SyntaxToken tokenToRight) GetTokensToLeftAndRight(
        SourceText sourceText, SyntaxNode root, int location)
    {
        // get Token for current location
        var tokenOnLocation = root.FindToken(location);
 
        var syntaxKinds = this.SyntaxFacts.SyntaxKinds;
        if (tokenOnLocation.RawKind == syntaxKinds.CommaToken && location >= tokenOnLocation.Span.End)
        {
            var commaToken = tokenOnLocation;
 
            // A couple of scenarios to care about:
            //
            //      X,$$ Y
            //
            // In this case, consider the user on the Y node.
            //
            //      X,$$
            //      Y
            //
            // In this case, consider the user on the X node.
            var nextToken = commaToken.GetNextToken();
            var previousToken = commaToken.GetPreviousToken();
            if (nextToken != default && !commaToken.TrailingTrivia.Any(t => t.RawKind == syntaxKinds.EndOfLineTrivia))
            {
                return (tokenToLeft: default, tokenToRight: nextToken);
            }
            else if (previousToken != default && previousToken.Span.End == commaToken.Span.Start)
            {
                return (tokenToLeft: previousToken, tokenToRight: default);
            }
        }
 
        // Gets a token that is directly to the right of current location or that encompasses current location (`[||]tokenToRightOrIn` or `tok[||]enToRightOrIn`)
        var tokenToRight = tokenOnLocation.Span.Contains(location)
            ? tokenOnLocation
            : default;
 
        // A token can be to the left only when there's either no tokenDirectlyToRightOrIn or there's one  directly starting at current location. 
        // Otherwise (otherwise tokenToRightOrIn is also left from location, e.g: `tok[||]enToRightOrIn`)
        var tokenToLeft = default(SyntaxToken);
        if (tokenToRight == default || tokenToRight.FullSpan.Start == location)
        {
            var previousToken = tokenOnLocation.Span.End == location
                ? tokenOnLocation
                : tokenOnLocation.GetPreviousToken(includeZeroWidth: true);
 
            tokenToLeft = previousToken.Span.End == location
                ? previousToken
                : default;
        }
 
        // If both tokens directly to left & right are empty -> we're somewhere in the middle of whitespace.
        // Since there wouldn't be (m)any other refactorings we can try to offer at least the ones for (semantically) 
        // closest token/Node. Thus, we move the location to the token in whose `.FullSpan` the original location was.
        if (tokenToLeft == default &&
            tokenToRight == default &&
            IsAcceptableLineDistanceAway(sourceText, tokenOnLocation, location))
        {
            // tokenOnLocation: token in whose trivia location is at
            if (tokenOnLocation.Span.Start >= location)
            {
                tokenToRight = tokenOnLocation;
            }
            else
            {
                tokenToLeft = tokenOnLocation;
            }
        }
 
        return (tokenToLeft, tokenToRight);
 
        static bool IsAcceptableLineDistanceAway(
            SourceText sourceText, SyntaxToken tokenOnLocation, int location)
        {
            // assume non-trivia token can't span multiple lines
            var tokenLine = sourceText.Lines.GetLineFromPosition(tokenOnLocation.Span.Start);
            var locationLine = sourceText.Lines.GetLineFromPosition(location);
 
            // Change location to nearest token only if the token is off by one line or less
            var lineDistance = tokenLine.LineNumber - locationLine.LineNumber;
            if (lineDistance is not 0 and not 1)
                return false;
 
            // Note: being a line below a tokenOnLocation is impossible in current model as whitespace 
            // trailing trivia ends on new line. Which is fine because if you're a line _after_ some node
            // you usually don't want refactorings for what's above you.
 
            if (lineDistance == 1)
            {
                // position is one line above the node of interest.  This is fine if that
                // line is blank.  Otherwise, if it isn't (i.e. it contains comments,
                // directives, or other trivia), then it's not likely the user is selecting
                // this entry.
                return locationLine.IsEmptyOrWhitespace();
            }
 
            // On hte same line.  This position is acceptable.
            return true;
        }
    }
 
    private void AddNodesForTokenToLeft<TSyntaxNode>(
        ISyntaxFacts syntaxFacts,
        bool allowEmptyNodes,
        int maxCount,
        ref TemporaryArray<TSyntaxNode> result,
        SyntaxToken tokenToLeft,
        CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        var location = tokenToLeft.Span.End;
 
        // there could be multiple (n) tokens to the left if first n-1 are Empty -> iterate over all of them
        while (tokenToLeft != default)
        {
            var leftNode = tokenToLeft.Parent;
            do
            {
                // Consider either a Node that is:
                // - Ancestor Node of such Token as long as their span ends on location (it's still on the edge)
                AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(leftNode, syntaxFacts), allowEmptyNodes, maxCount, ref result, cancellationToken);
                if (result.Count >= maxCount)
                    return;
 
                leftNode = leftNode?.Parent;
                if (leftNode is null)
                    break;
 
                if (leftNode.GetLastToken().Span.End != location && leftNode.Span.End != location)
                    break;
            }
            while (true);
 
            // as long as current tokenToLeft is empty -> its previous token is also tokenToLeft
            tokenToLeft = tokenToLeft.Span.IsEmpty
                ? tokenToLeft.GetPreviousToken(includeZeroWidth: true)
                : default;
        }
    }
 
    private void AddNodesForTokenToRight<TSyntaxNode>(
        ISyntaxFacts syntaxFacts,
        SyntaxNode root,
        bool allowEmptyNodes,
        int maxCount,
        ref TemporaryArray<TSyntaxNode> result,
        SyntaxToken tokenToRightOrIn,
        CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        var location = tokenToRightOrIn.Span.Start;
 
        if (tokenToRightOrIn != default)
        {
            var rightNode = tokenToRightOrIn.Parent;
            do
            {
                // Consider either a Node that is:
                // - Parent of touched Token (location can be within) 
                // - Ancestor Node of such Token as long as their span starts on location (it's still on the edge)
                AddNonHiddenCorrectTypeNodes(ExtractNodesSimple(rightNode, syntaxFacts), allowEmptyNodes, maxCount, ref result, cancellationToken);
                if (result.Count >= maxCount)
                    return;
 
                rightNode = rightNode?.Parent;
                if (rightNode == null)
                    break;
 
                // The edge climbing for node to the right needs to handle Attributes e.g.:
                // [Test1]
                // //Comment1
                // [||]object Property1 { get; set; }
                // In essence:
                // - On the left edge of the node (-> left edge of first AttributeLists)
                // - On the left edge of the node sans AttributeLists (& as everywhere comments)
                if (rightNode.Span.Start != location)
                {
                    var rightNodeSpanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, rightNode);
                    if (rightNodeSpanWithoutAttributes.Start != location)
                        break;
                }
            }
            while (true);
        }
    }
 
    private void AddRelevantNodesForSelection<TSyntaxNode>(
        ISyntaxFacts syntaxFacts,
        SyntaxNode root,
        TextSpan selectionTrimmed,
        bool allowEmptyNodes,
        int maxCount,
        ref TemporaryArray<TSyntaxNode> result,
        CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        var selectionNode = root.FindNode(selectionTrimmed, getInnermostNodeForTie: true);
        var prevNode = selectionNode;
        do
        {
            var nonHiddenExtractedSelectedNodes = ExtractNodesSimple(selectionNode, syntaxFacts).OfType<TSyntaxNode>().Where(n => !n.OverlapsHiddenPosition(cancellationToken));
            foreach (var nonHiddenExtractedNode in nonHiddenExtractedSelectedNodes)
            {
                // For selections we need to handle an edge case where only AttributeLists are within selection (e.g. `Func([|[in][out]|] arg1);`).
                // In that case the smallest encompassing node is still the whole argument node but it's hard to justify showing refactorings for it
                // if user selected only its attributes.
 
                // Selection contains only AttributeLists -> don't consider current Node
                var spanWithoutAttributes = syntaxFacts.GetSpanWithoutAttributes(root, nonHiddenExtractedNode);
                if (!selectionTrimmed.IntersectsWith(spanWithoutAttributes))
                {
                    break;
                }
 
                AddNode(allowEmptyNodes, ref result, nonHiddenExtractedNode);
                if (result.Count >= maxCount)
                    return;
            }
 
            prevNode = selectionNode;
            selectionNode = selectionNode.Parent;
        }
        while (selectionNode != null && prevNode.FullWidth() == selectionNode.FullWidth());
    }
 
    /// <summary>
    /// Extractor function that retrieves all nodes that should be considered for extraction of given current node. 
    /// <para>
    /// The rationale is that when user selects e.g. entire local declaration statement [|var a = b;|] it is reasonable
    /// to provide refactoring for `b` node. Similarly for other types of refactorings.
    /// </para>
    /// </summary>
    /// <remark>
    /// Should also return given node. 
    /// </remark>
    protected virtual IEnumerable<SyntaxNode> ExtractNodesSimple(SyntaxNode? node, ISyntaxFacts syntaxFacts)
    {
        if (node == null)
        {
            yield break;
        }
 
        // First return the node itself so that it is considered
        yield return node;
 
        // REMARKS: 
        // The set of currently attempted extractions is in no way exhaustive and covers only cases
        // that were found to be relevant for refactorings that were moved to `TryGetSelectedNodeAsync`.
        // Feel free to extend it / refine current heuristics. 
 
        // `var a = b;` | `var a = b`;
        if (syntaxFacts.IsLocalDeclarationStatement(node) || syntaxFacts.IsLocalDeclarationStatement(node.Parent))
        {
            var localDeclarationStatement = syntaxFacts.IsLocalDeclarationStatement(node) ? node : node.Parent!;
 
            // Check if there's only one variable being declared, otherwise following transformation
            // would go through which isn't reasonable since we can't say the first one specifically
            // is wanted.
            // `var a = 1, `c = 2, d = 3`;
            // -> `var a = 1`, c = 2, d = 3;
            var variables = syntaxFacts.GetVariablesOfLocalDeclarationStatement(localDeclarationStatement);
            if (variables.Count == 1)
            {
                var declaredVariable = variables.First();
 
                // -> `a = b`
                yield return declaredVariable;
 
                // -> `b`
                var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(declaredVariable);
                if (initializer != null)
                {
                    var value = syntaxFacts.GetValueOfEqualsValueClause(initializer);
                    if (value != null)
                    {
                        yield return value;
                    }
                }
            }
        }
 
        // var `a = b`;
        if (syntaxFacts.IsVariableDeclarator(node))
        {
            // -> `b`
            var initializer = syntaxFacts.GetInitializerOfVariableDeclarator(node);
            if (initializer != null)
            {
                var value = syntaxFacts.GetValueOfEqualsValueClause(initializer);
                if (value != null)
                {
                    yield return value;
                }
            }
        }
 
        // `a = b;`
        // -> `b`
        if (syntaxFacts.IsSimpleAssignmentStatement(node))
        {
            syntaxFacts.GetPartsOfAssignmentExpressionOrStatement(node, out _, out _, out var rightSide);
            yield return rightSide;
        }
 
        // `a();`
        // -> a()
        if (syntaxFacts.IsExpressionStatement(node))
        {
            yield return syntaxFacts.GetExpressionOfExpressionStatement(node);
        }
 
        // `a()`;
        // -> `a();`
        if (syntaxFacts.IsExpressionStatement(node.Parent))
        {
            yield return node.Parent;
        }
    }
 
    /// <summary>
    /// Extractor function that checks and retrieves all nodes current location is in a header.
    /// </summary>
    protected virtual IEnumerable<SyntaxNode> ExtractNodesInHeader(SyntaxNode root, int location, IHeaderFacts headerFacts)
    {
        // Header: [Test] `public int a` { get; set; }
        if (headerFacts.IsOnPropertyDeclarationHeader(root, location, out var propertyDeclaration))
            yield return propertyDeclaration;
 
        // Header: public C([Test]`int a = 42`) {}
        if (headerFacts.IsOnParameterHeader(root, location, out var parameter))
            yield return parameter;
 
        // Header: `public I.C([Test]int a = 42)` {}
        if (headerFacts.IsOnMethodHeader(root, location, out var method))
            yield return method;
 
        // Header: `static C([Test]int a = 42)` {}
        if (headerFacts.IsOnLocalFunctionHeader(root, location, out var localFunction))
            yield return localFunction;
 
        // Header: `var a = `3,` b = `5,` c = `7 + 3``;
        if (headerFacts.IsOnLocalDeclarationHeader(root, location, out var localDeclaration))
            yield return localDeclaration;
 
        // Header: `if(...)`{ };
        if (headerFacts.IsOnIfStatementHeader(root, location, out var ifStatement))
            yield return ifStatement;
 
        // Header: `foreach (var a in b)` { }
        if (headerFacts.IsOnForeachHeader(root, location, out var foreachStatement))
            yield return foreachStatement;
 
        if (headerFacts.IsOnTypeHeader(root, location, out var typeDeclaration))
            yield return typeDeclaration;
    }
 
    private static void AddNodesDeepIn<TSyntaxNode>(
        SourceText sourceText,
        SyntaxNode root,
        int position,
        bool allowEmptyNodes,
        int maxCount,
        ref TemporaryArray<TSyntaxNode> result,
        CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        // If we're deep inside we don't have to deal with being on edges (that gets dealt by TryGetSelectedNodeAsync)
        // -> can simply FindToken -> proceed testing its ancestors
        var token = root.FindTokenOnRightOfPosition(position, true);
 
        // traverse upwards and add all parents if of correct type
        var ancestor = token.Parent;
        while (ancestor != null)
        {
            if (ancestor is TSyntaxNode typedAncestor)
            {
                var argumentStartLine = sourceText.Lines.GetLineFromPosition(typedAncestor.Span.Start).LineNumber;
                var caretLine = sourceText.Lines.GetLineFromPosition(position).LineNumber;
 
                if (argumentStartLine == caretLine && !typedAncestor.OverlapsHiddenPosition(cancellationToken))
                {
                    AddNode(allowEmptyNodes, ref result, typedAncestor);
                    if (result.Count >= maxCount)
                        return;
                }
                else if (argumentStartLine < caretLine)
                {
                    // higher level nodes will have Span starting at least on the same line -> can bail out
                    return;
                }
            }
 
            ancestor = ancestor.Parent;
        }
    }
 
    private static void AddNonHiddenCorrectTypeNodes<TSyntaxNode>(
        IEnumerable<SyntaxNode> nodes, bool allowEmptyNodes, int maxCount, ref TemporaryArray<TSyntaxNode> result, CancellationToken cancellationToken) where TSyntaxNode : SyntaxNode
    {
        foreach (var node in nodes)
        {
            if (node is TSyntaxNode typedNode &&
                !node.OverlapsHiddenPosition(cancellationToken))
            {
                AddNode(allowEmptyNodes, ref result, typedNode);
                if (result.Count >= maxCount)
                    return;
            }
        }
    }
 
    public bool IsOnTypeHeader(SyntaxNode root, int position, bool fullHeader, [NotNullWhen(true)] out SyntaxNode? typeDeclaration)
        => HeaderFacts.IsOnTypeHeader(root, position, fullHeader, out typeDeclaration);
 
    public bool IsOnPropertyDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? propertyDeclaration)
        => HeaderFacts.IsOnPropertyDeclarationHeader(root, position, out propertyDeclaration);
 
    public bool IsOnParameterHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? parameter)
        => HeaderFacts.IsOnParameterHeader(root, position, out parameter);
 
    public bool IsOnMethodHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? method)
        => HeaderFacts.IsOnMethodHeader(root, position, out method);
 
    public bool IsOnLocalFunctionHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localFunction)
        => HeaderFacts.IsOnLocalFunctionHeader(root, position, out localFunction);
 
    public bool IsOnLocalDeclarationHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? localDeclaration)
        => HeaderFacts.IsOnLocalDeclarationHeader(root, position, out localDeclaration);
 
    public bool IsOnIfStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? ifStatement)
        => HeaderFacts.IsOnIfStatementHeader(root, position, out ifStatement);
 
    public bool IsOnWhileStatementHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? whileStatement)
        => HeaderFacts.IsOnWhileStatementHeader(root, position, out whileStatement);
 
    public bool IsOnForeachHeader(SyntaxNode root, int position, [NotNullWhen(true)] out SyntaxNode? foreachStatement)
        => HeaderFacts.IsOnForeachHeader(root, position, out foreachStatement);
}