|
// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.CSharp.Utilities;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
#pragma warning disable IDE0060 // Remove unused parameter - Majority of extension methods in this file have an unused 'SyntaxTree' this parameter for consistency with other Context related extension methods.
namespace Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
internal static partial class SyntaxTreeExtensions
{
private static readonly ISet<SyntaxKind> s_validLocalFunctionModifiers = new HashSet<SyntaxKind>(SyntaxFacts.EqualityComparer)
{
SyntaxKind.ExternKeyword,
SyntaxKind.StaticKeyword,
SyntaxKind.AsyncKeyword,
SyntaxKind.UnsafeKeyword,
};
public static bool IsAttributeNameContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
// cases:
// [ |
if (token.IsKind(SyntaxKind.OpenBracketToken) &&
token.Parent.IsKind(SyntaxKind.AttributeList))
{
return true;
}
// cases:
// [Goo(1), |
if (token.IsKind(SyntaxKind.CommaToken) &&
token.Parent.IsKind(SyntaxKind.AttributeList))
{
return true;
}
// cases:
// [specifier: |
if (token.IsKind(SyntaxKind.ColonToken) &&
token.Parent.IsKind(SyntaxKind.AttributeTargetSpecifier))
{
return true;
}
// cases:
// [Namespace.|
if (token.Parent.IsKind(SyntaxKind.QualifiedName) &&
token.Parent.IsParentKind(SyntaxKind.Attribute))
{
return true;
}
// cases:
// [global::|
if (token.Parent.IsKind(SyntaxKind.AliasQualifiedName) &&
token.Parent.IsParentKind(SyntaxKind.Attribute))
{
return true;
}
return false;
}
public static bool IsGlobalMemberDeclarationContext(
this SyntaxTree syntaxTree,
int position,
ISet<SyntaxKind> validModifiers,
CancellationToken cancellationToken)
{
if (!syntaxTree.IsScript())
{
return false;
}
var tokenOnLeftOfPosition = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
var parent = token.Parent;
var modifierTokens = syntaxTree.GetPrecedingModifiers(position, cancellationToken);
if (modifierTokens.IsEmpty())
{
if (token.IsKind(SyntaxKind.CloseBracketToken)
&& parent is AttributeListSyntax attributeList
&& !IsGlobalAttributeList(attributeList))
{
// Allow empty modifier tokens if we have an attribute list
parent = attributeList.Parent;
}
else
{
return false;
}
}
if (modifierTokens.IsSubsetOf(validModifiers))
{
// the parent is the member
// the grandparent is the container of the member
// in interactive, it's possible that there might be an intervening "incomplete" member for partially
// typed declarations that parse ambiguously. For example, "internal e". It's also possible for a
// complete member to be parsed based on data after the caret, e.g. "unsafe $$ void L() { }".
if (parent.IsKind(SyntaxKind.CompilationUnit) ||
(parent is MemberDeclarationSyntax && parent.IsParentKind(SyntaxKind.CompilationUnit)))
{
return true;
}
}
return false;
// Local functions
static bool IsGlobalAttributeList(AttributeListSyntax attributeList)
{
if (attributeList.Target is { Identifier.RawKind: var kind })
{
return kind is ((int)SyntaxKind.AssemblyKeyword)
or ((int)SyntaxKind.ModuleKeyword);
}
return false;
}
}
public static bool IsMemberDeclarationContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
// cases:
// class C {
// |
// class C {
// void Goo() {
// }
// |
// class C {
// int i;
// |
// class C {
// [Goo]
// |
var originalToken = tokenOnLeftOfPosition;
var token = originalToken;
// If we're touching the right of an identifier, move back to
// previous token.
token = token.GetPreviousTokenIfTouchingWord(position);
// class C {
// |
if (token.IsKind(SyntaxKind.OpenBraceToken))
{
if (token.Parent is BaseTypeDeclarationSyntax)
{
return true;
}
}
// class C {
// int i;
// |
if (token.IsKind(SyntaxKind.SemicolonToken))
{
if (token.Parent is MemberDeclarationSyntax &&
token.Parent.Parent is BaseTypeDeclarationSyntax)
{
return true;
}
}
// class A {
// class C {}
// |
// class C {
// void Goo() {
// }
// |
if (token.IsKind(SyntaxKind.CloseBraceToken))
{
if (token.Parent is BaseTypeDeclarationSyntax &&
token.Parent.Parent is BaseTypeDeclarationSyntax)
{
// after a nested type
return true;
}
else if (token.Parent is AccessorListSyntax)
{
// after a property
return true;
}
else if (
token.Parent.IsKind(SyntaxKind.Block) &&
token.Parent.Parent is MemberDeclarationSyntax)
{
// after a method/operator/etc.
return true;
}
}
// namespace Goo {
// [Bar]
// |
if (token.IsKind(SyntaxKind.CloseBracketToken) &&
token.Parent.IsKind(SyntaxKind.AttributeList))
{
// attributes belong to a member which itself is in a
// container.
// the parent is the attribute
// the grandparent is the owner of the attribute
// the great-grandparent is the container that the owner is in
var container = token.Parent.Parent?.Parent;
if (container is BaseTypeDeclarationSyntax)
{
return true;
}
}
return false;
}
public static bool IsMemberDeclarationContext(
this SyntaxTree syntaxTree,
int position,
CSharpSyntaxContext? context,
ISet<SyntaxKind>? validModifiers,
ISet<SyntaxKind>? validTypeDeclarations,
bool canBePartial,
CancellationToken cancellationToken)
{
var typeDecl = context != null
? context.ContainingTypeOrEnumDeclaration
: syntaxTree.GetContainingTypeOrEnumDeclaration(position, cancellationToken);
if (typeDecl == null)
{
return false;
}
validTypeDeclarations ??= SpecializedCollections.EmptySet<SyntaxKind>();
if (!validTypeDeclarations.Contains(typeDecl.Kind()))
{
return false;
}
// Check many of the simple cases first.
var leftToken = context != null
? context.LeftToken
: syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
var token = context != null
? context.TargetToken
: leftToken.GetPreviousTokenIfTouchingWord(position);
if (token.IsAnyAccessorDeclarationContext(position))
{
return false;
}
if (syntaxTree.IsMemberDeclarationContext(position, leftToken))
{
return true;
}
// A member can also show up after certain types of modifiers
if (canBePartial &&
token.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword))
{
return true;
}
var modifierTokens = context?.PrecedingModifiers ?? syntaxTree.GetPrecedingModifiers(position, cancellationToken);
if (modifierTokens.IsEmpty())
return false;
validModifiers ??= SpecializedCollections.EmptySet<SyntaxKind>();
if (modifierTokens.IsSubsetOf(validModifiers))
{
var member = token.Parent;
if (token.HasMatchingText(SyntaxKind.AsyncKeyword))
{
// rule out async lambdas inside a method
if (token.GetAncestor<StatementSyntax>() == null)
{
member = token.GetAncestor<MemberDeclarationSyntax>();
}
}
// cases:
// public |
// async |
// public async |
return member != null &&
member.Parent is BaseTypeDeclarationSyntax;
}
return false;
}
public static bool IsLambdaDeclarationContext(
this SyntaxTree syntaxTree,
int position,
SyntaxKind otherModifier,
CancellationToken cancellationToken)
{
var modifierTokens = syntaxTree.GetPrecedingModifiers(position, cancellationToken, out position);
if (modifierTokens.Count >= 2)
return false;
if (modifierTokens.Count == 1)
return modifierTokens.Contains(otherModifier) && IsLambdaDeclarationContext(syntaxTree, position, SyntaxKind.None, cancellationToken);
var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
return syntaxTree.IsExpressionContext(position, leftToken, attributes: false, cancellationToken);
}
public static bool IsLocalFunctionDeclarationContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
=> IsLocalFunctionDeclarationContext(syntaxTree, position, s_validLocalFunctionModifiers, cancellationToken);
public static bool IsLocalFunctionDeclarationContext(
this SyntaxTree syntaxTree,
int position,
ISet<SyntaxKind> validModifiers,
CancellationToken cancellationToken)
{
var modifierTokens = syntaxTree.GetPrecedingModifiers(position, cancellationToken, out position);
// if we had modifiers, they have to be legal in this context.
if (!modifierTokens.IsSubsetOf(validModifiers))
return false;
// if we had modifiers, restart the search at the point prior to them.
if (modifierTokens.Count > 0)
return IsLocalFunctionDeclarationContext(syntaxTree, position, validModifiers, cancellationToken);
var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
var token = leftToken.GetPreviousTokenIfTouchingWord(position);
// if we're after an attribute, restart the check at teh start of the attribute.
if (token.Kind() == SyntaxKind.CloseBracketToken && token.Parent is AttributeListSyntax)
return syntaxTree.IsLocalFunctionDeclarationContext(token.Parent.SpanStart, validModifiers, cancellationToken);
if (syntaxTree.IsStatementContext(position, leftToken, cancellationToken))
return true;
return !syntaxTree.IsScript() && syntaxTree.IsGlobalStatementContext(position, cancellationToken);
}
public static bool IsTypeDeclarationContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
// cases:
// root: |
// extern alias a;
// |
// using Goo;
// |
// using Goo = Bar;
// |
// namespace N {}
// |
// namespace N {
// |
// class C {}
// |
// class C {
// |
// class C {
// void Goo() {
// }
// |
// class C {
// int i;
// |
// class C {
// [Goo]
// |
// (all the class cases apply to structs, interfaces and records).
var originalToken = tokenOnLeftOfPosition;
var token = originalToken;
// If we're touching the right of an identifier, move back to
// previous token.
token = token.GetPreviousTokenIfTouchingWord(position);
// a type decl can't come before usings/externs
var nextToken = originalToken.GetNextToken(includeSkipped: true);
if (nextToken.IsUsingOrExternKeyword() ||
(nextToken.Kind() == SyntaxKind.GlobalKeyword && nextToken.GetAncestor<UsingDirectiveSyntax>()?.GlobalKeyword == nextToken))
{
return false;
}
// root: |
if (token.IsKind(SyntaxKind.None))
{
// root namespace
// a type decl can't come before usings/externs
if (syntaxTree.GetRoot(cancellationToken) is CompilationUnitSyntax compilationUnit &&
(compilationUnit.Externs.Count > 0 || compilationUnit.Usings.Count > 0))
{
return false;
}
return true;
}
if (token.IsKind(SyntaxKind.OpenBraceToken) && token.Parent is NamespaceDeclarationSyntax or TypeDeclarationSyntax)
return true;
// extern alias a;
// |
// using Goo;
// |
// class C {
// int i;
// |
// namespace NS;
// |
if (token.IsKind(SyntaxKind.SemicolonToken))
{
if (token.Parent is (kind: SyntaxKind.ExternAliasDirective or SyntaxKind.UsingDirective))
{
return true;
}
else if (token.Parent is MemberDeclarationSyntax)
{
return true;
}
}
// class C {}
// |
// namespace N {}
// |
// class C {
// void Goo() {
// }
// |
if (token.IsKind(SyntaxKind.CloseBraceToken))
{
if (token.Parent is BaseTypeDeclarationSyntax)
{
return true;
}
else if (token.Parent.IsKind(SyntaxKind.NamespaceDeclaration))
{
return true;
}
else if (token.Parent is AccessorListSyntax)
{
return true;
}
else if (
token.Parent.IsKind(SyntaxKind.Block) &&
token.Parent.Parent is MemberDeclarationSyntax)
{
return true;
}
}
// namespace Goo {
// [Bar]
// |
// namespace NS;
// [Attr]
// |
if (token.IsKind(SyntaxKind.CloseBracketToken) &&
token.Parent.IsKind(SyntaxKind.AttributeList))
{
// assembly attributes belong to the containing compilation unit
if (token.Parent.IsParentKind(SyntaxKind.CompilationUnit))
return true;
// other attributes belong to a member which itself is in a
// container.
// the parent is the attribute
// the grandparent is the owner of the attribute
// the great-grandparent is the container that the owner is in
var container = token.Parent?.Parent?.Parent;
if (container is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax or TypeDeclarationSyntax)
return true;
}
return false;
}
public static bool IsTypeDeclarationContext(
this SyntaxTree syntaxTree,
int position,
CSharpSyntaxContext? context,
ISet<SyntaxKind>? validModifiers,
ISet<SyntaxKind>? validTypeDeclarations,
bool canBePartial,
CancellationToken cancellationToken)
{
// We only allow nested types inside a class, struct, or interface, not inside a
// an enum.
var typeDecl = context != null
? context.ContainingTypeDeclaration
: syntaxTree.GetContainingTypeDeclaration(position, cancellationToken);
validTypeDeclarations ??= SpecializedCollections.EmptySet<SyntaxKind>();
if (typeDecl != null)
{
if (!validTypeDeclarations.Contains(typeDecl.Kind()))
{
return false;
}
}
// Check many of the simple cases first.
var leftToken = context != null
? context.LeftToken
: syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
// If we're touching the right of an identifier, move back to
// previous token.
var token = context != null
? context.TargetToken
: leftToken.GetPreviousTokenIfTouchingWord(position);
if (token.IsAnyAccessorDeclarationContext(position))
{
return false;
}
if (syntaxTree.IsTypeDeclarationContext(position, leftToken, cancellationToken))
{
return true;
}
// A type can also show up after certain types of modifiers
if (canBePartial &&
token.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword))
{
return true;
}
// using directive is never a type declaration context
if (token.GetAncestor<UsingDirectiveSyntax>() is not null)
{
return false;
}
var modifierTokens = context?.PrecedingModifiers ?? syntaxTree.GetPrecedingModifiers(position, cancellationToken);
if (modifierTokens.IsEmpty())
return false;
validModifiers ??= SpecializedCollections.EmptySet<SyntaxKind>();
if (modifierTokens.IsProperSubsetOf(validModifiers))
{
// the parent is the member
// the grandparent is the container of the member
var container = token.Parent?.Parent;
// ref $$
// readonly ref $$
if (container is IncompleteMemberSyntax incompleteMember)
return incompleteMember.Type.IsKind(SyntaxKind.RefType);
if (container is CompilationUnitSyntax or BaseNamespaceDeclarationSyntax or TypeDeclarationSyntax)
return true;
if (container is VariableDeclarationSyntax && modifierTokens.Contains(SyntaxKind.FileKeyword))
return true;
}
return false;
}
public static bool IsNamespaceContext(
this SyntaxTree syntaxTree,
int position,
CancellationToken cancellationToken,
SemanticModel? semanticModel = null)
{
// first do quick exit check
if (syntaxTree.IsInNonUserCode(position, cancellationToken) ||
syntaxTree.IsRightOfDotOrArrow(position, cancellationToken))
{
return false;
}
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken)
.GetPreviousTokenIfTouchingWord(position);
// global::
if (token.IsKind(SyntaxKind.ColonColonToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.GlobalKeyword))
{
return true;
}
// using |
// but not:
// using | = Bar
// Note: we take care of the using alias case in the IsTypeContext
// call below.
if (token.IsKind(SyntaxKind.UsingKeyword))
{
var usingDirective = token.GetAncestor<UsingDirectiveSyntax>();
if (usingDirective != null)
{
if (token.GetNextToken(includeSkipped: true).Kind() != SyntaxKind.EqualsToken &&
usingDirective.Alias == null)
{
return true;
}
}
}
// using static |
// using static unsafe |
if (token.IsStaticKeywordContextInUsingDirective())
{
return true;
}
// if it is not using directive location, most of places where
// type can appear, namespace can appear as well
return syntaxTree.IsTypeContext(position, cancellationToken, semanticModel);
}
public static bool IsNamespaceDeclarationNameContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
if (syntaxTree.IsScript() || syntaxTree.IsInNonUserCode(position, cancellationToken))
return false;
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken)
.GetPreviousTokenIfTouchingWord(position);
if (token == default)
return false;
var declaration = token.GetAncestor<BaseNamespaceDeclarationSyntax>();
if (declaration?.NamespaceKeyword == token)
return true;
return declaration?.Name.Span.IntersectsWith(position) == true;
}
public static bool IsPartialTypeDeclarationNameContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken, [NotNullWhen(true)] out TypeDeclarationSyntax? declarationSyntax)
{
if (!syntaxTree.IsInNonUserCode(position, cancellationToken))
{
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken)
.GetPreviousTokenIfTouchingWord(position);
if (token.Kind()
is SyntaxKind.ClassKeyword
or SyntaxKind.StructKeyword
or SyntaxKind.InterfaceKeyword &&
token.GetPreviousToken().IsKind(SyntaxKind.PartialKeyword))
{
declarationSyntax = token.GetAncestor<TypeDeclarationSyntax>();
return declarationSyntax != null && declarationSyntax.Keyword == token;
}
}
declarationSyntax = null;
return false;
}
public static bool IsDefinitelyNotTypeContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
if (syntaxTree.IsPreProcessorDirectiveContext(position, cancellationToken))
return true;
return
syntaxTree.IsInNonUserCode(position, cancellationToken) ||
syntaxTree.IsRightOfDotOrArrow(position, cancellationToken);
}
public static bool IsTypeContext(
this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken, SemanticModel? semanticModel = null)
{
// first do quick exit check
if (syntaxTree.IsDefinitelyNotTypeContext(position, cancellationToken))
return false;
// okay, now it is a case where we can't use parse tree (valid or error recovery) to
// determine whether it is a right place to put type. use lex based one Cyrus created.
var tokenOnLeftOfPosition = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
return
syntaxTree.IsAfterKeyword(position, SyntaxKind.ConstKeyword, cancellationToken) ||
syntaxTree.IsAfterKeyword(position, SyntaxKind.RefKeyword, cancellationToken) ||
syntaxTree.IsAfterKeyword(position, SyntaxKind.ReadOnlyKeyword, cancellationToken) ||
syntaxTree.IsAfterKeyword(position, SyntaxKind.CaseKeyword, cancellationToken) ||
syntaxTree.IsAfterKeyword(position, SyntaxKind.EventKeyword, cancellationToken) ||
syntaxTree.IsAfterKeyword(position, SyntaxKind.StackAllocKeyword, cancellationToken) ||
syntaxTree.IsAttributeNameContext(position, cancellationToken) ||
syntaxTree.IsBaseClassOrInterfaceContext(position, cancellationToken) ||
syntaxTree.IsCatchVariableDeclarationContext(position, cancellationToken) ||
syntaxTree.IsDefiniteCastTypeContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsDelegateReturnTypeContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsExpressionContext(position, tokenOnLeftOfPosition, attributes: true, cancellationToken: cancellationToken, semanticModel: semanticModel) ||
syntaxTree.IsPrimaryFunctionExpressionContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsGenericTypeArgumentContext(position, tokenOnLeftOfPosition, cancellationToken, semanticModel) ||
syntaxTree.IsFunctionPointerTypeArgumentContext(position, tokenOnLeftOfPosition, cancellationToken) ||
syntaxTree.IsFixedVariableDeclarationContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsImplicitOrExplicitOperatorTypeContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsIsOrAsTypeContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsLocalVariableDeclarationContext(position, tokenOnLeftOfPosition, cancellationToken) ||
syntaxTree.IsObjectCreationTypeContext(position, tokenOnLeftOfPosition, cancellationToken) ||
syntaxTree.IsParameterTypeContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsPossibleLambdaOrAnonymousMethodParameterTypeContext(position, tokenOnLeftOfPosition, cancellationToken) ||
syntaxTree.IsStatementContext(position, tokenOnLeftOfPosition, cancellationToken) ||
syntaxTree.IsGlobalStatementContext(position, cancellationToken) ||
syntaxTree.IsTypeParameterConstraintContext(position, tokenOnLeftOfPosition) ||
syntaxTree.IsUsingAliasTypeContext(position, cancellationToken) ||
syntaxTree.IsUsingStaticContext(position, cancellationToken) ||
syntaxTree.IsGlobalMemberDeclarationContext(position, SyntaxKindSet.AllGlobalMemberModifiers, cancellationToken) ||
syntaxTree.IsPossibleTupleContext(tokenOnLeftOfPosition, position) ||
syntaxTree.IsMemberDeclarationContext(
position,
context: null,
validModifiers: SyntaxKindSet.AllMemberModifiers,
validTypeDeclarations: SyntaxKindSet.ClassInterfaceStructRecordTypeDeclarations,
canBePartial: false,
cancellationToken: cancellationToken) ||
syntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken);
}
public static bool IsBaseClassOrInterfaceContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
// class C : |
// class C : Bar, |
// NOT enum E : |
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.Kind() is SyntaxKind.ColonToken or SyntaxKind.CommaToken &&
token.Parent is BaseListSyntax { Parent: not EnumDeclarationSyntax })
{
return true;
}
return false;
}
public static bool IsUsingAliasTypeContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
// using Goo = |
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.EqualsToken) &&
token.GetAncestor<UsingDirectiveSyntax>() != null)
{
return true;
}
return false;
}
public static bool IsUsingStaticContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
// using static |
// using static unsafe |
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
return token.IsStaticKeywordContextInUsingDirective();
}
public static bool IsTypeArgumentOfConstraintClause(
this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
// cases:
// where |
// class Goo<T> : Object where |
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.WhereKeyword) &&
token.Parent.IsKind(SyntaxKind.TypeParameterConstraintClause))
{
return true;
}
if (token.IsKind(SyntaxKind.IdentifierToken) &&
token.HasMatchingText(SyntaxKind.WhereKeyword) &&
token.Parent.IsKind(SyntaxKind.IdentifierName) &&
token.Parent.IsParentKind(SyntaxKind.SimpleBaseType) &&
token.Parent.Parent.IsParentKind(SyntaxKind.BaseList))
{
return true;
}
return false;
}
public static bool IsTypeParameterConstraintStartContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
// cases:
// where T : |
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.ColonToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.IdentifierToken) &&
token.GetPreviousToken(includeSkipped: true).GetPreviousToken().IsKind(SyntaxKind.WhereKeyword))
{
return true;
}
return false;
}
public static bool IsTypeParameterConstraintContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
if (syntaxTree.IsTypeParameterConstraintStartContext(position, tokenOnLeftOfPosition))
{
return true;
}
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
// Can't come after new()
//
// where T : |
// where T : class, |
// where T : struct, |
// where T : Goo, |
if (token.IsKind(SyntaxKind.CommaToken) &&
token.Parent is TypeParameterConstraintClauseSyntax constraintClause)
{
// Check if there's a 'new()' constraint. If there isn't, or we're before it, then
// this is a type parameter constraint context.
var firstConstructorConstraint = constraintClause.Constraints.FirstOrDefault(t => t is ConstructorConstraintSyntax);
if (firstConstructorConstraint == null || firstConstructorConstraint.SpanStart > token.Span.End)
{
return true;
}
}
return false;
}
public static bool IsTypeOfExpressionContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OpenParenToken) && token.Parent.IsKind(SyntaxKind.TypeOfExpression))
{
return true;
}
return false;
}
public static bool IsDefaultExpressionContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OpenParenToken) && token.Parent.IsKind(SyntaxKind.DefaultExpression))
{
return true;
}
return false;
}
public static bool IsSizeOfExpressionContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OpenParenToken) && token.Parent.IsKind(SyntaxKind.SizeOfExpression))
{
return true;
}
return false;
}
public static bool IsFunctionPointerTypeArgumentContext(
this SyntaxTree syntaxTree,
int position,
SyntaxToken tokenOnLeftOfPosition,
CancellationToken cancellationToken)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
switch (token.Kind())
{
case SyntaxKind.LessThanToken:
case SyntaxKind.CommaToken:
return token.Parent.IsKind(SyntaxKind.FunctionPointerParameterList);
}
return token switch
{
// ref modifiers
{ Parent.RawKind: (int)SyntaxKind.FunctionPointerParameter } => true,
// Regular type specifiers
{ Parent: TypeSyntax { Parent.RawKind: (int)SyntaxKind.FunctionPointerParameter } } => true,
_ => false
};
}
public static bool IsGenericConstraintContext(this SyntaxTree syntaxTree, SyntaxToken targetToken)
=> targetToken.Parent.IsKind(SyntaxKind.TypeParameterConstraintClause) &&
targetToken.Kind() is SyntaxKind.ColonToken or SyntaxKind.CommaToken;
public static bool IsGenericTypeArgumentContext(
this SyntaxTree syntaxTree,
int position,
SyntaxToken tokenOnLeftOfPosition,
CancellationToken cancellationToken,
SemanticModel? semanticModelOpt = null)
{
// cases:
// Goo<|
// Goo<Bar,|
// Goo<Bar<Baz<int[],|
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.Kind() is not SyntaxKind.LessThanToken and not SyntaxKind.CommaToken)
{
return false;
}
if (token.Parent is TypeArgumentListSyntax)
{
// Easy case, it was known to be a generic name, so this is a type argument context.
return true;
}
if (!syntaxTree.IsInPartiallyWrittenGeneric(position, cancellationToken, out var nameToken))
{
return false;
}
if (nameToken.Parent is not NameSyntax name)
{
return false;
}
// Looks viable! If they provided a binding, then check if it binds properly to
// an actual generic entity.
if (semanticModelOpt == null)
{
// No binding. Just make the decision based on the syntax tree.
return true;
}
// '?' is syntactically ambiguous in incomplete top-level statements:
//
// T ? goo<|
//
// Might be an incomplete conditional expression or an incomplete declaration of a method returning a nullable type.
// Bind T to see if it is a type. If it is we don't show signature help.
if (name.IsParentKind(SyntaxKind.LessThanExpression) &&
name.Parent?.Parent is ConditionalExpressionSyntax conditional &&
conditional.IsParentKind(SyntaxKind.ExpressionStatement) &&
conditional.Parent.IsParentKind(SyntaxKind.GlobalStatement))
{
var conditionOrType = semanticModelOpt.GetSymbolInfo(conditional.Condition, cancellationToken);
if (conditionOrType.GetBestOrAllSymbols().FirstOrDefault() is { Kind: SymbolKind.NamedType })
{
return false;
}
}
// We have reached the expression:
//
// goo.Baz<|
//
// This could either be an incomplete generic type or method, or a binary less than operator
// To ensure that we are in the generic case, we need to match at least one generic method or type,
// and all other candidates to be types or methods.
var symbols = semanticModelOpt.LookupName(nameToken, cancellationToken);
if (symbols.Length == 0)
return false;
var anyGeneric = symbols.Any(static s =>
{
return s switch
{
INamedTypeSymbol nt => nt.Arity > 0,
IMethodSymbol m => m.Arity > 0,
_ => false,
};
});
if (!anyGeneric)
return false;
return symbols.All(static s => s is INamedTypeSymbol or IMethodSymbol);
}
public static bool IsParameterModifierContext(
this SyntaxTree syntaxTree,
int position,
SyntaxToken tokenOnLeftOfPosition,
bool includeOperators,
out int parameterIndex,
out SyntaxKind previousModifier)
{
// cases:
// Goo(|
// Goo(int i, |
// Goo([Bar]|
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
parameterIndex = -1;
previousModifier = SyntaxKind.None;
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.Parent is ParameterListSyntax parameterList1 &&
IsSuitableParameterList(parameterList1, includeOperators))
{
parameterIndex = 0;
return true;
}
if (token.IsKind(SyntaxKind.LessThanToken) && token.Parent.IsKind(SyntaxKind.FunctionPointerParameterList))
{
parameterIndex = 0;
return true;
}
if (token.IsKind(SyntaxKind.CommaToken) &&
token.Parent is ParameterListSyntax parameterList2 &&
IsSuitableParameterList(parameterList2, includeOperators))
{
var commaIndex = parameterList2.Parameters.GetWithSeparators().IndexOf(token);
parameterIndex = commaIndex / 2 + 1;
return true;
}
if (token.IsKind(SyntaxKind.CommaToken) &&
token.Parent is FunctionPointerParameterListSyntax funcPtrParamList)
{
var commaIndex = funcPtrParamList.Parameters.GetWithSeparators().IndexOf(token);
parameterIndex = commaIndex / 2 + 1;
return true;
}
if (token.IsKind(SyntaxKind.CloseBracketToken) &&
token.Parent.IsKind(SyntaxKind.AttributeList) &&
token.Parent.Parent is ParameterSyntax parameter3 &&
parameter3.Parent is ParameterListSyntax parameterList3 &&
IsSuitableParameterList(parameterList3, includeOperators))
{
parameterIndex = parameterList3.Parameters.IndexOf(parameter3);
return true;
}
ParameterSyntax? parameter4 = null;
if (token.Kind() is SyntaxKind.RefKeyword or SyntaxKind.InKeyword or SyntaxKind.ReadOnlyKeyword or SyntaxKind.OutKeyword or SyntaxKind.ThisKeyword or SyntaxKind.ParamsKeyword or SyntaxKind.ScopedKeyword)
{
parameter4 = token.Parent as ParameterSyntax;
previousModifier = token.Kind();
}
else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "scoped" && token.Parent is IdentifierNameSyntax scopedIdentifierName)
{
parameter4 = scopedIdentifierName.Parent as ParameterSyntax;
previousModifier = SyntaxKind.ScopedKeyword;
}
if (parameter4 is { Parent: ParameterListSyntax parameterList4 } &&
IsSuitableParameterList(parameterList4, includeOperators))
{
parameterIndex = parameterList4.Parameters.IndexOf(parameter4);
return true;
}
return false;
static bool IsSuitableParameterList(ParameterListSyntax parameterList, bool includeOperators)
=> parameterList.Parent switch
{
MethodDeclarationSyntax or LocalFunctionStatementSyntax or ConstructorDeclarationSyntax or DelegateDeclarationSyntax or TypeDeclarationSyntax => true,
OperatorDeclarationSyntax or ConversionOperatorDeclarationSyntax when includeOperators => true,
_ => false,
};
}
public static bool IsParamsModifierContext(
this SyntaxTree syntaxTree,
int position,
SyntaxToken tokenOnLeftOfPosition,
CancellationToken cancellationToken)
{
if (syntaxTree.IsParameterModifierContext(position, tokenOnLeftOfPosition, includeOperators: false, out _, out var previousModifier) &&
previousModifier == SyntaxKind.None)
{
return true;
}
if (syntaxTree.IsPossibleLambdaParameterModifierContext(position, tokenOnLeftOfPosition, cancellationToken))
{
return true;
}
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.Kind() is SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken)
{
return token.Parent.IsKind(SyntaxKind.BracketedParameterList);
}
return false;
}
public static bool IsDelegateReturnTypeContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.DelegateKeyword) &&
token.Parent.IsKind(SyntaxKind.DelegateDeclaration))
{
return true;
}
return false;
}
public static bool IsImplicitOrExplicitOperatorTypeContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OperatorKeyword) &&
token.GetPreviousToken(includeSkipped: true).Kind() is SyntaxKind.ImplicitKeyword or SyntaxKind.ExplicitKeyword)
{
return true;
}
return false;
}
public static bool IsParameterTypeContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (syntaxTree.IsParameterModifierContext(position, tokenOnLeftOfPosition, includeOperators: true, out _, out _))
return true;
// int this[ |
// int this[int i, |
if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken &&
token.Parent is (kind: SyntaxKind.ParameterList or SyntaxKind.BracketedParameterList))
{
return true;
}
return false;
}
public static bool IsPossibleExtensionMethodContext(this SyntaxTree syntaxTree, SyntaxToken tokenOnLeftOfPosition)
{
var method = tokenOnLeftOfPosition.Parent.GetAncestorOrThis<MethodDeclarationSyntax>();
var typeDecl = method.GetAncestorOrThis<TypeDeclarationSyntax>();
return method != null && typeDecl != null &&
typeDecl.IsKind(SyntaxKind.ClassDeclaration) &&
method.Modifiers.Any(SyntaxKind.StaticKeyword) &&
typeDecl.Modifiers.Any(SyntaxKind.StaticKeyword);
}
public static bool IsPossibleLambdaParameterModifierContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken)
{
if (token.Parent.IsKind(SyntaxKind.ParameterList) &&
token.Parent.IsParentKind(SyntaxKind.ParenthesizedLambdaExpression))
{
return true;
}
// TODO(cyrusn): Tie into semantic analysis system to only consider this a lambda if this is a location
// where the lambda's type would be inferred because of a delegate or Expression<T> type.
//
// ERROR tolerance. Cast expressions can show up with partially written lambdas like so:
//
// var lambda = (x$$)
// NextStatement();
//
// Check if the expression of the cast is on the same line as us or not to see if we want to
// consider this a lambda, or just a cast.
if (token.Parent is (kind: SyntaxKind.ParenthesizedExpression or SyntaxKind.TupleExpression or SyntaxKind.CastExpression))
return true;
}
return false;
}
public static bool IsAnonymousMethodParameterModifierContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
SyntaxNode? parent;
if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken)
{
parent = token.Parent;
}
else if (token.IsKind(SyntaxKind.ScopedKeyword) && token.Parent.IsKind(SyntaxKind.Parameter))
{
parent = token.Parent.Parent;
}
else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "scoped" && token.Parent is IdentifierNameSyntax scopedIdentifierName && scopedIdentifierName.Parent.IsKind(SyntaxKind.Parameter))
{
parent = scopedIdentifierName.Parent.Parent;
}
else
{
return false;
}
return parent.IsKind(SyntaxKind.ParameterList) && parent.IsParentKind(SyntaxKind.AnonymousMethodExpression);
}
public static bool IsPossibleLambdaOrAnonymousMethodParameterTypeContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.Kind() is SyntaxKind.RefKeyword or SyntaxKind.InKeyword or SyntaxKind.OutKeyword)
{
position = token.SpanStart;
tokenOnLeftOfPosition = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
}
if (IsAnonymousMethodParameterModifierContext(syntaxTree, position, tokenOnLeftOfPosition) ||
IsPossibleLambdaParameterModifierContext(syntaxTree, position, tokenOnLeftOfPosition, cancellationToken))
{
return true;
}
return false;
}
/// <summary>
/// Are you possibly typing a tuple type or expression?
/// This is used to suppress colon as a completion trigger (so that you can type element names).
/// This is also used to recommend some keywords (like var).
/// </summary>
public static bool IsPossibleTupleContext(this SyntaxTree syntaxTree, SyntaxToken leftToken, int position)
{
leftToken = leftToken.GetPreviousTokenIfTouchingWord(position);
// ($$
// (a, $$
if (IsPossibleTupleOpenParenOrComma(leftToken))
{
return true;
}
// ((a, b) $$
// (..., (a, b) $$
if (leftToken.IsKind(SyntaxKind.CloseParenToken))
{
if (leftToken.Parent is (kind:
SyntaxKind.ParenthesizedExpression or
SyntaxKind.TupleExpression or
SyntaxKind.TupleType))
{
var possibleCommaOrParen = FindTokenOnLeftOfNode(leftToken.Parent);
if (IsPossibleTupleOpenParenOrComma(possibleCommaOrParen))
{
return true;
}
}
}
// (a $$
// (..., b $$
if (leftToken.IsKind(SyntaxKind.IdentifierToken))
{
var possibleCommaOrParen = FindTokenOnLeftOfNode(leftToken.Parent!);
if (IsPossibleTupleOpenParenOrComma(possibleCommaOrParen))
{
return true;
}
}
// (a.b $$
// (..., a.b $$
if (leftToken.IsKind(SyntaxKind.IdentifierToken) &&
leftToken.Parent.IsKind(SyntaxKind.IdentifierName) &&
leftToken.Parent.Parent is (kind: SyntaxKind.QualifiedName or SyntaxKind.SimpleMemberAccessExpression))
{
var possibleCommaOrParen = FindTokenOnLeftOfNode(leftToken.Parent.Parent);
if (IsPossibleTupleOpenParenOrComma(possibleCommaOrParen))
{
return true;
}
}
return false;
}
public static bool IsAtStartOfPattern(this SyntaxTree syntaxTree, SyntaxToken leftToken, int position)
{
leftToken = leftToken.GetPreviousTokenIfTouchingWord(position);
if (leftToken.IsKind(SyntaxKind.OpenParenToken))
{
if (leftToken.Parent is ParenthesizedExpressionSyntax parenthesizedExpression)
{
// If we're dealing with an expression surrounded by one or more sets of open parentheses, we need to
// walk up the parens in order to see if we're actually at the start of a valid pattern or not.
return IsAtStartOfPattern(syntaxTree, parenthesizedExpression.GetFirstToken().GetPreviousToken(), parenthesizedExpression.SpanStart);
}
// e is ((($$ 1 or 2)))
if (leftToken.Parent.IsKind(SyntaxKind.ParenthesizedPattern))
{
return true;
}
}
// case $$
// is $$
if (leftToken.Kind() is SyntaxKind.CaseKeyword or SyntaxKind.IsKeyword)
{
return true;
}
// e switch { $$
// e switch { ..., $$
if (leftToken.Kind() is SyntaxKind.OpenBraceToken or SyntaxKind.CommaToken && leftToken.Parent.IsKind(SyntaxKind.SwitchExpression))
{
return true;
}
// e is ($$
// e is (..., $$
if (leftToken.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken && leftToken.Parent.IsKind(SyntaxKind.PositionalPatternClause))
{
return true;
}
// e is [$$
// e is [..., $$
if (leftToken.Kind() is SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken && leftToken.Parent.IsKind(SyntaxKind.ListPattern))
{
return true;
}
// e is [..$$
// e is [..., ..$$
if (leftToken.IsKind(SyntaxKind.DotDotToken) && leftToken.Parent.IsKind(SyntaxKind.SlicePattern))
{
return true;
}
// e is { P: $$
// e is { ..., P: $$
// e is { ..., P.P2: $$
if (leftToken.IsKind(SyntaxKind.ColonToken) && leftToken.Parent is (kind: SyntaxKind.NameColon or SyntaxKind.ExpressionColon) &&
leftToken.Parent.IsParentKind(SyntaxKind.Subpattern))
{
return true;
}
// e is 1 and $$
// e is 1 or $$
// e is SomeEnum.SomeEnumValue and $$
// e is SomeEnum.SomeEnumValue or $$
// 'and' & 'or' are identifier in the last 2 examples because of lack of context
if (leftToken.IsKindOrHasMatchingText(SyntaxKind.AndKeyword) || leftToken.IsKindOrHasMatchingText(SyntaxKind.OrKeyword))
{
return leftToken.Parent is BinaryPatternSyntax ||
leftToken.Parent is SingleVariableDesignationSyntax { Parent: DeclarationPatternSyntax };
}
// e is not $$
if (leftToken.IsKind(SyntaxKind.NotKeyword) && leftToken.Parent.IsKind(SyntaxKind.NotPattern))
{
return true;
}
// e is > $$
// e is >= $$
// e is < $$
// e is <= $$
if (leftToken.Kind() is SyntaxKind.GreaterThanToken or SyntaxKind.GreaterThanEqualsToken or SyntaxKind.LessThanToken or SyntaxKind.LessThanEqualsToken &&
leftToken.Parent.IsKind(SyntaxKind.RelationalPattern))
{
return true;
}
return false;
}
public static bool IsAtEndOfPattern(this SyntaxTree syntaxTree, SyntaxToken leftToken, int position)
{
var originalLeftToken = leftToken;
leftToken = leftToken.GetPreviousTokenIfTouchingWord(position);
// For instance:
// e is { A.$$ }
// e is { A->$$ }
if (leftToken.IsKind(SyntaxKind.DotToken) ||
leftToken.IsKind(SyntaxKind.MinusGreaterThanToken))
{
return false;
}
var patternSyntax = leftToken.GetAncestor<PatternSyntax>();
if (patternSyntax != null)
{
var lastTokenInPattern = patternSyntax.GetLastToken();
// This check should cover the majority of cases, e.g.:
// e is 1 $$
// e is >= 0 $$
// e is { P: (1 $$
// e is { P: (1) $$
if (leftToken == lastTokenInPattern)
{
// Patterns such as 'e is not $$', 'e is 1 or $$', 'e is ($$', and 'e is null or global::$$' should be invalid here
// as they are incomplete patterns.
return leftToken.Kind() is not (SyntaxKind.OrKeyword
or SyntaxKind.AndKeyword
or SyntaxKind.NotKeyword
or SyntaxKind.OpenParenToken
or SyntaxKind.ColonColonToken
or SyntaxKind.DotDotToken
or SyntaxKind.OpenBraceToken);
}
// We want to make sure that IsAtEndOfPattern returns true even when the user is in the middle of typing a keyword
// after a pattern.
// For example, with the keyword 'and', we want to make sure that 'e is int an$$' is still recognized as valid.
if (lastTokenInPattern.Parent is SingleVariableDesignationSyntax variableDesignationSyntax &&
originalLeftToken.Parent == variableDesignationSyntax)
{
return patternSyntax is DeclarationPatternSyntax or RecursivePatternSyntax;
}
// e is (expr) a$$
//
// this will be parsed as a constant-pattern where the constant expression is a cast expression (if 'expr'
// is a legal type).
if (patternSyntax is ConstantPatternSyntax { Expression: CastExpressionSyntax { Expression: IdentifierNameSyntax } castExpression } &&
leftToken == castExpression.CloseParenToken)
{
return true;
}
}
// e is C.P $$
// e is int $$
if (leftToken.IsLastTokenOfNode<TypeSyntax>(out var typeSyntax))
{
// If typeSyntax is part of a qualified name, we want to get the fully-qualified name so that we can
// later accurately perform the check comparing the right side of the BinaryExpressionSyntax to
// the typeSyntax.
while (typeSyntax.Parent is TypeSyntax parentTypeSyntax)
{
typeSyntax = parentTypeSyntax;
}
if (typeSyntax.Parent is BinaryExpressionSyntax binaryExpressionSyntax &&
binaryExpressionSyntax.OperatorToken.IsKind(SyntaxKind.IsKeyword) &&
binaryExpressionSyntax.Right == typeSyntax && !typeSyntax.IsVar)
{
return true;
}
}
// We need to include a special case for switch statement cases, as some are not currently parsed as patterns, e.g. case (1 $$
if (IsAtEndOfSwitchStatementPattern(leftToken))
{
return true;
}
return false;
static bool IsAtEndOfSwitchStatementPattern(SyntaxToken leftToken)
{
SyntaxNode? node = leftToken.Parent as ExpressionSyntax;
if (node == null)
return false;
// Walk up the right edge of all complete expressions.
while (node is ExpressionSyntax && node.GetLastToken(includeZeroWidth: true) == leftToken)
node = node.GetRequiredParent();
// Getting rid of the extra parentheses to deal with cases such as 'case (((1 $$'
while (node is ParenthesizedExpressionSyntax)
node = node.GetRequiredParent();
// case (1 $$
if (node is CaseSwitchLabelSyntax { Parent: SwitchSectionSyntax })
return true;
return false;
}
}
private static SyntaxToken FindTokenOnLeftOfNode(SyntaxNode node)
=> node.FindTokenOnLeftOfPosition(node.SpanStart);
public static bool IsPossibleTupleOpenParenOrComma(this SyntaxToken possibleCommaOrParen)
{
if (possibleCommaOrParen.Kind() is not (SyntaxKind.OpenParenToken or SyntaxKind.CommaToken))
{
return false;
}
if (possibleCommaOrParen.Parent is (kind:
SyntaxKind.ParenthesizedExpression or
SyntaxKind.TupleExpression or
SyntaxKind.TupleType or
SyntaxKind.CastExpression))
{
return true;
}
// in script
if (possibleCommaOrParen.Parent.IsKind(SyntaxKind.ParameterList) &&
possibleCommaOrParen.Parent?.Parent is ParenthesizedLambdaExpressionSyntax parenthesizedLambda)
{
if (parenthesizedLambda.ArrowToken.IsMissing)
{
return true;
}
}
return false;
}
/// <summary>
/// Are you possibly in the designation part of a deconstruction?
/// This is used to enter suggestion mode (suggestions become soft-selected).
/// </summary>
public static bool IsPossibleDeconstructionDesignation(this SyntaxTree syntaxTree,
int position, CancellationToken cancellationToken)
{
var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
leftToken = leftToken.GetPreviousTokenIfTouchingWord(position);
// The well-formed cases:
// var ($$, y) = e;
// (var $$, var y) = e;
if (leftToken.Parent.IsKind(SyntaxKind.ParenthesizedVariableDesignation) ||
leftToken.Parent.IsParentKind(SyntaxKind.ParenthesizedVariableDesignation))
{
return true;
}
// (var $$, var y)
// (var x, var y)
if (syntaxTree.IsPossibleTupleContext(leftToken, position) && !IsPossibleTupleOpenParenOrComma(leftToken))
{
return true;
}
// var ($$)
// var (x, $$)
if (IsPossibleVarDeconstructionOpenParenOrComma(leftToken))
{
return true;
}
// var (($$), y)
if (leftToken.IsKind(SyntaxKind.OpenParenToken) && leftToken.Parent.IsKind(SyntaxKind.ParenthesizedExpression))
{
if (IsPossibleVarDeconstructionOpenParenOrComma(FindTokenOnLeftOfNode(leftToken.Parent)))
{
return true;
}
}
// var ((x, $$), y)
// var (($$, x), y)
if (leftToken.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken && leftToken.Parent.IsKind(SyntaxKind.TupleExpression))
{
if (IsPossibleVarDeconstructionOpenParenOrComma(FindTokenOnLeftOfNode(leftToken.Parent)))
{
return true;
}
}
// foreach (var ($$
// foreach (var ((x, $$), y)
if (leftToken.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken)
{
var outer = UnwrapPossibleTuple(leftToken.Parent!);
if (outer.Parent is ForEachStatementSyntax @foreach)
{
if (@foreach.Expression == outer &&
@foreach.Type is IdentifierNameSyntax identifierName &&
identifierName.Identifier.ValueText == "var")
{
return true;
}
}
}
return false;
}
/// <summary>
/// If inside a parenthesized or tuple expression, unwrap the nestings and return the container.
/// </summary>
private static SyntaxNode UnwrapPossibleTuple(SyntaxNode node)
{
while (true)
{
if (node.Parent.IsKind(SyntaxKind.ParenthesizedExpression))
{
node = node.Parent;
continue;
}
if (node.Parent.IsKind(SyntaxKind.Argument) && node.Parent.Parent.IsKind(SyntaxKind.TupleExpression))
{
node = node.Parent.Parent;
continue;
}
return node;
}
}
private static bool IsPossibleVarDeconstructionOpenParenOrComma(SyntaxToken leftToken)
{
if (leftToken.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken &&
leftToken.Parent.IsKind(SyntaxKind.ArgumentList) &&
leftToken.Parent?.Parent is InvocationExpressionSyntax invocation)
{
if (invocation.Expression is IdentifierNameSyntax identifierName &&
identifierName.Identifier.ValueText == "var")
{
return true;
}
}
return false;
}
public static bool HasNames(this TupleExpressionSyntax tuple)
=> tuple.Arguments.Any(a => a.NameColon != null);
public static bool IsValidContextForFromClause(
this SyntaxTree syntaxTree,
int position,
SyntaxToken tokenOnLeftOfPosition,
CancellationToken cancellationToken,
SemanticModel? semanticModelOpt = null)
{
if (syntaxTree.IsExpressionContext(position, tokenOnLeftOfPosition, attributes: false, cancellationToken: cancellationToken, semanticModel: semanticModelOpt) &&
!syntaxTree.IsConstantExpressionContext(position, tokenOnLeftOfPosition))
{
return true;
}
// cases:
// var q = |
// var q = f|
//
// var q = from x in y
// |
//
// var q = from x in y
// f|
//
// this list is *not* exhaustive.
// the first two are handled by 'IsExpressionContext'
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
// var q = from x in y
// |
if (!token.IntersectsWith(position) &&
token.IsLastTokenOfQueryClause())
{
return true;
}
return false;
}
public static bool IsValidContextForJoinClause(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
// var q = from x in y
// |
if (!token.IntersectsWith(position) &&
token.IsLastTokenOfQueryClause())
{
return true;
}
return false;
}
public static bool IsLocalVariableDeclarationContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
// cases:
// const var
// out var
// for (var
// foreach (var
// await foreach (var
// using (var
// await using (var
// from var
// join var
// using var
// await using var
// scoped var
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
// const |
if (token.IsKind(SyntaxKind.ConstKeyword) &&
token.Parent.IsKind(SyntaxKind.LocalDeclarationStatement))
{
return true;
}
// ref |
// ref readonly |
// for ( ref |
// foreach ( ref | x
if (token.Kind() is SyntaxKind.RefKeyword or SyntaxKind.ReadOnlyKeyword)
{
var parent = token.Parent;
if (parent is (kind: SyntaxKind.RefType or SyntaxKind.RefExpression or SyntaxKind.LocalDeclarationStatement))
{
if (parent.IsParentKind(SyntaxKind.VariableDeclaration) &&
parent.Parent?.Parent is (kind:
SyntaxKind.LocalDeclarationStatement or
SyntaxKind.ForStatement or
SyntaxKind.ForEachVariableStatement))
{
return true;
}
if (parent.Parent is (kind: SyntaxKind.ForEachStatement or SyntaxKind.ForEachVariableStatement))
{
return true;
}
}
}
// out |
if (token.IsKind(SyntaxKind.OutKeyword) &&
token.Parent is ArgumentSyntax argument &&
argument.RefKindKeyword == token)
{
return true;
}
// for ( |
// foreach ( |
// await foreach ( |
// using ( |
// await using ( |
if (token.IsKind(SyntaxKind.OpenParenToken))
{
var previous = token.GetPreviousToken(includeSkipped: true);
if (previous.Kind() is SyntaxKind.ForKeyword or SyntaxKind.ForEachKeyword or SyntaxKind.UsingKeyword)
return true;
}
// using |
// await using |
if (token.IsKind(SyntaxKind.UsingKeyword) &&
token.Parent is LocalDeclarationStatementSyntax)
{
return true;
}
// from |
var tokenOnLeftOfStart = syntaxTree.FindTokenOnLeftOfPosition(token.SpanStart, cancellationToken);
if (token.IsKindOrHasMatchingText(SyntaxKind.FromKeyword) &&
syntaxTree.IsValidContextForFromClause(token.SpanStart, tokenOnLeftOfStart, cancellationToken))
{
return true;
}
// join |
if (CodeAnalysis.CSharpExtensions.IsKind(token, SyntaxKind.JoinKeyword) &&
syntaxTree.IsValidContextForJoinClause(token.SpanStart, tokenOnLeftOfStart))
{
return true;
}
// scoped |
// The compiler parses this as an identifier whose parent is:
// - ExpressionStatementSyntax when in method declaration.
// - IncompleteMemberSyntax when in top-level code and there are no class declarations after it.
// - BaseTypeDeclarationSyntax if it comes after scoped
// - VariableDeclarationSyntax for `scoped X` inside method declaration
if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "scoped" && token.Parent.IsKind(SyntaxKind.IdentifierName) && token.Parent.Parent is VariableDeclarationSyntax or ExpressionStatementSyntax or IncompleteMemberSyntax)
{
return true;
}
// scoped v|
if (token.IsKind(SyntaxKind.ScopedKeyword) && token.Parent is IncompleteMemberSyntax)
{
return true;
}
return false;
}
public static bool IsFixedVariableDeclarationContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
// cases:
// fixed (var
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.FixedKeyword))
{
return true;
}
return false;
}
public static bool IsCatchVariableDeclarationContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
// cases:
// catch (var
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.CatchKeyword))
{
return true;
}
return false;
}
public static bool IsIsOrAsTypeContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.Kind() is SyntaxKind.IsKeyword or SyntaxKind.AsKeyword)
return true;
return false;
}
public static bool IsObjectCreationTypeContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.NewKeyword))
{
// we can follow a 'new' if it's the 'new' for an expression.
var start = token.SpanStart;
var tokenOnLeftOfStart = syntaxTree.FindTokenOnLeftOfPosition(start, cancellationToken);
return
IsNonConstantExpressionContext(syntaxTree, token.SpanStart, tokenOnLeftOfStart, cancellationToken) ||
syntaxTree.IsStatementContext(token.SpanStart, tokenOnLeftOfStart, cancellationToken) ||
syntaxTree.IsGlobalStatementContext(token.SpanStart, cancellationToken);
}
return false;
}
private static bool IsNonConstantExpressionContext(SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
return
syntaxTree.IsExpressionContext(position, tokenOnLeftOfPosition, attributes: true, cancellationToken: cancellationToken) &&
!syntaxTree.IsConstantExpressionContext(position, tokenOnLeftOfPosition);
}
public static bool IsPreProcessorDirectiveContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken, includeDirectives: true);
return syntaxTree.IsPreProcessorDirectiveContext(position, leftToken, cancellationToken);
}
public static bool IsStatementContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
#if false
// we're in a statement if the thing that comes before allows for
// statements to follow. Or if we're on a just started identifier
// in the first position where a statement can go.
if (syntaxTree.IsInPreprocessorDirectiveContext(position, cancellationToken))
{
return false;
}
#endif
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
return token.IsBeginningOfStatementContext();
}
public static bool IsGlobalStatementContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
if (syntaxTree.IsPreProcessorDirectiveContext(position, cancellationToken))
return false;
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken)
.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.None))
{
// global statements can't come before usings/externs
if (syntaxTree.GetRoot(cancellationToken) is CompilationUnitSyntax compilationUnit &&
(compilationUnit.Externs.Count > 0 || compilationUnit.Usings.Count > 0))
{
return false;
}
return true;
}
return token.IsBeginningOfGlobalStatementContext();
}
public static bool IsInstanceContext(this SyntaxTree syntaxTree, SyntaxToken targetToken, SemanticModel semanticModel, CancellationToken cancellationToken)
{
#if false
if (syntaxTree.IsInPreprocessorDirectiveContext(position, cancellationToken))
{
return false;
}
#endif
// It's possible the caller is asking about a speculative semantic model, and may have moved before the
// bounds of that model (for example, while looking at the nearby tokens around an edit). If so, ensure we
// walk outwards to the correct model to actually ask this question of.
var position = targetToken.SpanStart;
if (semanticModel.IsSpeculativeSemanticModel && position < semanticModel.OriginalPositionForSpeculation)
semanticModel = semanticModel.GetOriginalSemanticModel();
var enclosingSymbol = semanticModel.GetEnclosingSymbol(position, cancellationToken);
while (enclosingSymbol is IMethodSymbol { MethodKind: MethodKind.LocalFunction or MethodKind.AnonymousFunction } method)
{
if (method.IsStatic)
return false;
// It is allowed to reference the instance (`this`) within a local function or anonymous function, as long as the containing method allows it
enclosingSymbol = enclosingSymbol.ContainingSymbol;
}
return enclosingSymbol is { IsStatic: false };
}
public static bool IsPossibleCastTypeContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken)
{
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (CodeAnalysis.CSharpExtensions.IsKind(token, SyntaxKind.OpenParenToken) &&
syntaxTree.IsExpressionContext(token.SpanStart, syntaxTree.FindTokenOnLeftOfPosition(token.SpanStart, cancellationToken), false, cancellationToken))
{
return true;
}
return false;
}
public static bool IsDefiniteCastTypeContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.Parent.IsKind(SyntaxKind.CastExpression))
{
return true;
}
return false;
}
public static bool IsConstantExpressionContext(this SyntaxTree syntaxTree, int position,
SyntaxToken tokenOnLeftOfPosition)
{
if (IsAtStartOfPattern(syntaxTree, tokenOnLeftOfPosition, position))
{
return true;
}
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
// goto case |
if (token.IsKind(SyntaxKind.CaseKeyword) &&
token.Parent.IsKind(SyntaxKind.GotoCaseStatement))
{
return true;
}
if (token.IsKind(SyntaxKind.EqualsToken) &&
token.Parent is EqualsValueClauseSyntax equalsValue)
{
if (equalsValue.IsParentKind(SyntaxKind.VariableDeclarator) &&
equalsValue.Parent.IsParentKind(SyntaxKind.VariableDeclaration))
{
// class C { const int i = |
var fieldDeclaration = equalsValue.GetAncestor<FieldDeclarationSyntax>();
if (fieldDeclaration != null)
{
return fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword);
}
// void M() { const int i = |
var localDeclaration = equalsValue.GetAncestor<LocalDeclarationStatementSyntax>();
if (localDeclaration != null)
{
return localDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword);
}
}
// enum E { A = |
if (equalsValue.IsParentKind(SyntaxKind.EnumMemberDeclaration))
{
return true;
}
// void M(int i = |
if (equalsValue.IsParentKind(SyntaxKind.Parameter))
{
return true;
}
}
// [Goo( |
// [Goo(x, |
if (token.Parent.IsKind(SyntaxKind.AttributeArgumentList) &&
token.Kind() is SyntaxKind.CommaToken or SyntaxKind.OpenParenToken)
{
return true;
}
// [Goo(x: |
if (token.IsKind(SyntaxKind.ColonToken) &&
token.Parent.IsKind(SyntaxKind.NameColon) &&
token.Parent.IsParentKind(SyntaxKind.AttributeArgument))
{
return true;
}
// [Goo(X = |
if (token.IsKind(SyntaxKind.EqualsToken) &&
token.Parent.IsKind(SyntaxKind.NameEquals) &&
token.Parent.IsParentKind(SyntaxKind.AttributeArgument))
{
return true;
}
// TODO: Fixed-size buffer declarations
return false;
}
public static bool IsLabelContext(this SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
{
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
var gotoStatement = token.GetAncestor<GotoStatementSyntax>();
if (gotoStatement != null)
{
if (gotoStatement.GotoKeyword == token)
{
return true;
}
if (gotoStatement.Expression != null &&
!gotoStatement.Expression.IsMissing &&
gotoStatement.Expression is IdentifierNameSyntax &&
((IdentifierNameSyntax)gotoStatement.Expression).Identifier == token &&
token.IntersectsWith(position))
{
return true;
}
}
return false;
}
public static bool IsExpressionContext(
this SyntaxTree syntaxTree,
int position,
SyntaxToken tokenOnLeftOfPosition,
bool attributes,
CancellationToken cancellationToken,
SemanticModel? semanticModel = null)
{
// cases:
// var q = |
// var q = a|
// this list is *not* exhaustive.
var token = tokenOnLeftOfPosition.GetPreviousTokenIfTouchingWord(position);
if (token.GetAncestor<ConditionalDirectiveTriviaSyntax>() != null)
{
return false;
}
if (!attributes)
{
if (token.GetAncestor<AttributeListSyntax>() != null)
{
return false;
}
}
if (syntaxTree.IsConstantExpressionContext(position, tokenOnLeftOfPosition))
{
return true;
}
// no expressions after . :: ->
if (token.Kind()
is SyntaxKind.DotToken
or SyntaxKind.ColonColonToken
or SyntaxKind.MinusGreaterThanToken)
{
return false;
}
// Normally you can have any sort of expression after an equals. However, this does not
// apply to a "using Goo = ..." situation.
if (token.IsKind(SyntaxKind.EqualsToken))
{
if (token.Parent.IsKind(SyntaxKind.NameEquals) &&
token.Parent.IsParentKind(SyntaxKind.UsingDirective))
{
return false;
}
}
// q = |
// q -= |
// q *= |
// q += |
// q /= |
// q ^= |
// q %= |
// q &= |
// q |= |
// q <<= |
// q >>= |
// q >>>= |
// q ??= |
if (token.Kind()
is SyntaxKind.EqualsToken
or SyntaxKind.MinusEqualsToken
or SyntaxKind.AsteriskEqualsToken
or SyntaxKind.PlusEqualsToken
or SyntaxKind.SlashEqualsToken
or SyntaxKind.ExclamationEqualsToken
or SyntaxKind.CaretEqualsToken
or SyntaxKind.AmpersandEqualsToken
or SyntaxKind.BarEqualsToken
or SyntaxKind.PercentEqualsToken
or SyntaxKind.LessThanLessThanEqualsToken
or SyntaxKind.GreaterThanGreaterThanEqualsToken
or SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken
or SyntaxKind.QuestionQuestionEqualsToken)
{
return true;
}
// ( |
if (token.IsKind(SyntaxKind.OpenParenToken))
{
if (token.Parent.IsKind(SyntaxKind.ParenthesizedExpression))
return true;
// If there's a string in the parenthesis in the code below, the parser would return
// a CastExpression instead of ParenthesizedExpression. However, some features like keyword completion
// might be able tolerate this and still want to treat it as a ParenthesizedExpression.
//
// var data = (n$$)
// M();
if (token.Parent is CastExpressionSyntax castExpression &&
(castExpression.Expression.IsMissing || castExpression.CloseParenToken.TrailingTrivia.GetFirstNewLine().HasValue))
{
return true;
}
}
// - |
// + |
// ~ |
// ! |
if (token.Parent is PrefixUnaryExpressionSyntax prefix)
{
return prefix.OperatorToken == token;
}
// not sure about these:
// ++ |
// -- |
#if false
token.Kind == SyntaxKind.PlusPlusToken ||
token.Kind == SyntaxKind.DashDashToken)
#endif
// await |
if (token.Parent is AwaitExpressionSyntax awaitExpression)
{
return awaitExpression.AwaitKeyword == token;
}
// Check for binary operators.
// Note:
// - We handle < specially as it can be ambiguous with generics.
// - We handle * specially because it can be ambiguous with pointer types.
// a *
// a /
// a %
// a +
// a -
// a <<
// a >>
// a <
// a >
// a &&
// a ||
// a &
// a |
// a ^
if (token.Parent is BinaryExpressionSyntax binary)
{
// If the client provided a binding, then check if this is actually generic. If so,
// then this is not an expression context. i.e. if we have "Goo < |" then it could
// be an expression context, or it could be a type context if Goo binds to a type or
// method.
if (semanticModel != null && syntaxTree.IsGenericTypeArgumentContext(position, tokenOnLeftOfPosition, cancellationToken, semanticModel))
{
return false;
}
if (binary.OperatorToken == token)
{
// If this is a multiplication expression and a semantic model was passed in,
// check to see if the expression to the left is a type name. If it is, treat
// this as a pointer type.
if (token.IsKind(SyntaxKind.AsteriskToken) && semanticModel != null)
{
if (binary.Left is TypeSyntax type && type.IsPotentialTypeName(semanticModel, cancellationToken))
{
return false;
}
}
return true;
}
}
// Special case:
// Goo * bar
// Goo ? bar
// This parses as a local decl called bar of type Goo* or Goo?
if (tokenOnLeftOfPosition.IntersectsWith(position) &&
tokenOnLeftOfPosition.IsKind(SyntaxKind.IdentifierToken))
{
var previousToken = tokenOnLeftOfPosition.GetPreviousToken(includeSkipped: true);
if (previousToken.Kind() is SyntaxKind.AsteriskToken or SyntaxKind.QuestionToken &&
previousToken.Parent?.Kind() is SyntaxKind.PointerType or SyntaxKind.NullableType)
{
var type = previousToken.Parent as TypeSyntax;
if (type.IsParentKind(SyntaxKind.VariableDeclaration) &&
type.Parent?.Parent is LocalDeclarationStatementSyntax declStatement)
{
// note, this doesn't apply for cases where we know it
// absolutely is not multiplication or a conditional expression.
var underlyingType = type is PointerTypeSyntax pointerType
? pointerType.ElementType
: ((NullableTypeSyntax)type).ElementType;
if (!underlyingType.IsPotentialTypeName(semanticModel, cancellationToken))
{
return true;
}
}
}
}
// new int[|
// new int[expr, |
if (token.Kind() is SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken &&
token.Parent.IsKind(SyntaxKind.ArrayRankSpecifier))
{
return true;
}
// 1..|
// not 1.|.
if (token.IsKind(SyntaxKind.DotDotToken) &&
token.Parent.IsKind(SyntaxKind.RangeExpression) &&
position >= token.Span.End)
{
return true;
}
// goo ? |
if (token.IsKind(SyntaxKind.QuestionToken) &&
token.Parent is ConditionalExpressionSyntax conditionalExpression)
{
// If the condition is simply a TypeSyntax that binds to a type, treat this as a nullable type.
return conditionalExpression.Condition is not TypeSyntax type
|| !type.IsPotentialTypeName(semanticModel, cancellationToken);
}
// goo ? bar : |
if (token.IsKind(SyntaxKind.ColonToken) &&
token.Parent.IsKind(SyntaxKind.ConditionalExpression))
{
return true;
}
// typeof(|
// default(|
// sizeof(|
if (token.IsKind(SyntaxKind.OpenParenToken))
{
if (token.Parent is (kind: SyntaxKind.TypeOfExpression or SyntaxKind.DefaultExpression or SyntaxKind.SizeOfExpression))
{
return false;
}
}
// var(|
// var(id, |
// Those are more likely to be deconstruction-declarations being typed than invocations a method "var"
if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken &&
token.IsInvocationOfVarExpression())
{
return false;
}
// Goo(|
// Goo(expr, |
// this[|
// var t = (1, |
// var t = (| , 2)
if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken &&
token.Parent is (kind: SyntaxKind.ArgumentList or SyntaxKind.BracketedArgumentList or SyntaxKind.TupleExpression))
{
return true;
}
// [Goo(|
// [Goo(expr, |
if (attributes)
{
if (token.Kind() is SyntaxKind.OpenParenToken or SyntaxKind.CommaToken)
{
if (token.Parent.IsKind(SyntaxKind.AttributeArgumentList))
{
return true;
}
}
}
// Goo(ref |
// Goo(in |
// Goo(out |
// ref var x = ref |
if (token.Kind() is SyntaxKind.RefKeyword or SyntaxKind.InKeyword or SyntaxKind.OutKeyword)
{
if (token.Parent.IsKind(SyntaxKind.Argument))
{
return true;
}
else if (token.Parent.IsKind(SyntaxKind.RefExpression))
{
// ( ref |
// parenthesized expressions can't directly contain RefExpression, unless the user is typing an incomplete lambda expression.
if (token.Parent.IsParentKind(SyntaxKind.ParenthesizedExpression))
{
return false;
}
return true;
}
}
// Goo(bar: |
if (token.IsKind(SyntaxKind.ColonToken) &&
token.Parent.IsKind(SyntaxKind.NameColon) &&
token.Parent.IsParentKind(SyntaxKind.Argument))
{
return true;
}
// a => |
if (token.IsKind(SyntaxKind.EqualsGreaterThanToken))
{
return true;
}
// new List<int> { |
// new List<int> { expr, |
if (token.Kind() is SyntaxKind.OpenBraceToken or SyntaxKind.CommaToken)
{
if (token.Parent is InitializerExpressionSyntax)
{
// The compiler treats the ambiguous case as an object initializer, so we'll say
// expressions are legal here
if (token.Parent.IsKind(SyntaxKind.ObjectInitializerExpression) && token.IsKind(SyntaxKind.OpenBraceToken))
{
// In this position { a$$ =, the user is trying to type an object initializer.
if (!token.IntersectsWith(position) && token.GetNextToken().GetNextToken().IsKind(SyntaxKind.EqualsToken))
{
return false;
}
return true;
}
// Perform a semantic check to determine whether or not the type being created
// can support a collection initializer. If not, this must be an object initializer
// and can't be an expression context.
if (semanticModel != null &&
token.Parent?.Parent is ObjectCreationExpressionSyntax objectCreation)
{
var containingSymbol = semanticModel.GetEnclosingNamedTypeOrAssembly(position, cancellationToken);
if (semanticModel.GetSymbolInfo(objectCreation.Type, cancellationToken).Symbol is ITypeSymbol type && !type.CanSupportCollectionInitializer(containingSymbol))
{
return false;
}
}
return true;
}
}
// for (; |
// for (; ; |
if (token.IsKind(SyntaxKind.SemicolonToken) &&
token.Parent is ForStatementSyntax forStatement)
{
if (token == forStatement.FirstSemicolonToken ||
token == forStatement.SecondSemicolonToken)
{
return true;
}
}
// for ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.Parent is ForStatementSyntax forStatement2 &&
token == forStatement2.OpenParenToken)
{
return true;
}
// for (; ; Goo(), |
// for ( Goo(), |
if (token.IsKind(SyntaxKind.CommaToken) &&
token.Parent.IsKind(SyntaxKind.ForStatement))
{
return true;
}
// foreach (var v in |
// await foreach (var v in |
// from a in |
// join b in |
if (token.IsKind(SyntaxKind.InKeyword))
{
if (token.Parent is (kind:
SyntaxKind.ForEachStatement or
SyntaxKind.ForEachVariableStatement or
SyntaxKind.FromClause or
SyntaxKind.JoinClause))
{
return true;
}
}
// join x in y on |
// join x in y on a equals |
if (token.Kind() is SyntaxKind.OnKeyword or SyntaxKind.EqualsKeyword &&
token.Parent.IsKind(SyntaxKind.JoinClause))
{
return true;
}
// where |
if (token.IsKind(SyntaxKind.WhereKeyword) &&
token.Parent.IsKind(SyntaxKind.WhereClause))
{
return true;
}
// orderby |
// orderby a, |
if (token.Kind() is SyntaxKind.OrderByKeyword or SyntaxKind.CommaToken &&
token.Parent.IsKind(SyntaxKind.OrderByClause))
{
return true;
}
// select |
if (token.IsKind(SyntaxKind.SelectKeyword) &&
token.Parent.IsKind(SyntaxKind.SelectClause))
{
return true;
}
// group |
// group expr by |
if (token.Kind() is SyntaxKind.GroupKeyword or SyntaxKind.ByKeyword &&
token.Parent.IsKind(SyntaxKind.GroupClause))
{
return true;
}
// return |
// yield return |
// but not: [return |
if (token.IsKind(SyntaxKind.ReturnKeyword))
{
if (token.GetPreviousToken(includeSkipped: true).Kind() != SyntaxKind.OpenBracketToken)
{
return true;
}
}
// throw |
if (token.IsKind(SyntaxKind.ThrowKeyword))
{
return true;
}
// while ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.WhileKeyword))
{
return true;
}
// todo: handle 'for' cases.
// using ( |
// await using ( |
if (token.IsKind(SyntaxKind.OpenParenToken) && token.Parent.IsKind(SyntaxKind.UsingStatement))
{
return true;
}
// lock ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.LockKeyword))
{
return true;
}
// lock ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.IfKeyword))
{
return true;
}
// switch ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.SwitchKeyword))
{
return true;
}
// checked ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.CheckedKeyword))
{
return true;
}
// unchecked ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.UncheckedKeyword))
{
return true;
}
// when ( |
if (token.IsKind(SyntaxKind.OpenParenToken) &&
token.GetPreviousToken(includeSkipped: true).IsKind(SyntaxKind.WhenKeyword))
{
return true;
}
// case ... when |
if (token.IsKind(SyntaxKind.WhenKeyword) && token.Parent.IsKind(SyntaxKind.WhenClause))
{
return true;
}
// (SomeType) |
if (token.IsAfterPossibleCast())
{
return true;
}
// In anonymous type initializer.
//
// new { | We allow new inside of anonymous object member declarators, so that the user
// can dot into a member afterward. For example:
//
// var a = new { new C().Goo };
if (token.Kind() is SyntaxKind.OpenBraceToken or SyntaxKind.CommaToken &&
token.Parent.IsKind(SyntaxKind.AnonymousObjectCreationExpression))
{
return true;
}
// List patterns
// is [ |
// is [ 0, |
if (token.Kind() is SyntaxKind.OpenBracketToken or SyntaxKind.CommaToken &&
token.Parent.IsKind(SyntaxKind.ListPattern))
{
return true;
}
// Collection expressions
// [|
// [0, |
if (token.Kind() is SyntaxKind.OpenBracketToken or SyntaxKind.DotDotToken or SyntaxKind.CommaToken &&
token.Parent.IsKind(SyntaxKind.CollectionExpression))
{
return true;
}
// Spread elements in collection expressions
// [.. |
// [0, .. |
if (token.Kind() is SyntaxKind.DotDotToken &&
token.Parent.IsKind(SyntaxKind.SpreadElement))
{
return true;
}
// $"{ |
// $@"{ |
// $"""{ |
// $"{x} { |
// $@"{x} { |
// $"""{x} { |
if (token.IsKind(SyntaxKind.OpenBraceToken))
{
return token.Parent is InterpolationSyntax interpolation
&& interpolation.OpenBraceToken == token;
}
return false;
}
public static bool IsInvocationOfVarExpression(this SyntaxToken token)
=> token.Parent?.Parent is InvocationExpressionSyntax invocation &&
invocation.Expression.ToString() == "var";
public static bool IsNameOfContext(this SyntaxTree syntaxTree, int position, SemanticModel? semanticModelOpt = null, CancellationToken cancellationToken = default)
{
var token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken);
token = token.GetPreviousTokenIfTouchingWord(position);
// nameof(Goo.|
// nameof(Goo.Bar.|
// Locate the open paren.
if (token.IsKind(SyntaxKind.DotToken))
{
// Could have been parsed as member access
if (token.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression))
{
var parentMemberAccess = token.Parent;
while (parentMemberAccess.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression))
{
parentMemberAccess = parentMemberAccess.Parent;
}
if (parentMemberAccess.Parent.IsKind(SyntaxKind.Argument) &&
parentMemberAccess.Parent.IsChildNode<ArgumentListSyntax>(a => a.Arguments.FirstOrDefault()))
{
token = ((ArgumentListSyntax)parentMemberAccess.Parent.Parent!).OpenParenToken;
}
}
// Could have been parsed as a qualified name.
if (token.Parent.IsKind(SyntaxKind.QualifiedName))
{
var parentQualifiedName = token.Parent;
while (parentQualifiedName.Parent.IsKind(SyntaxKind.QualifiedName))
{
parentQualifiedName = parentQualifiedName.Parent;
}
if (parentQualifiedName.Parent.IsKind(SyntaxKind.Argument) &&
parentQualifiedName.Parent.IsChildNode<ArgumentListSyntax>(a => a.Arguments.FirstOrDefault()))
{
token = ((ArgumentListSyntax)parentQualifiedName.Parent.Parent!).OpenParenToken;
}
}
}
ExpressionSyntax? parentExpression = null;
// if the nameof expression has a missing close paren, it is parsed as an invocation expression.
if (token.Parent.IsKind(SyntaxKind.ArgumentList) &&
token.Parent.Parent is InvocationExpressionSyntax invocationExpression &&
invocationExpression.IsNameOfInvocation())
{
parentExpression = invocationExpression;
}
if (parentExpression != null)
{
if (semanticModelOpt == null)
{
return true;
}
return semanticModelOpt.GetSymbolInfo(parentExpression, cancellationToken).Symbol == null;
}
return false;
}
public static bool IsIsOrAsOrSwitchOrWithExpressionContext(
this SyntaxTree syntaxTree,
SemanticModel semanticModel,
int position,
SyntaxToken tokenOnLeftOfPosition,
CancellationToken cancellationToken)
{
// cases:
// expr |
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
// Not if the position is a numeric literal
if (token.IsKind(SyntaxKind.NumericLiteralToken))
return false;
if (token.GetAncestor<BlockSyntax>() == null &&
token.GetAncestor<ArrowExpressionClauseSyntax>() == null)
{
return false;
}
// is/as/with are valid after expressions.
if (token.IsLastTokenOfNode<ExpressionSyntax>(out var expression))
{
// 'is/as/with' not allowed after a anonymous-method/lambda/method-group.
if (expression is AnonymousFunctionExpressionSyntax)
return false;
var symbol = semanticModel.GetSymbolInfo(expression, cancellationToken).GetAnySymbol();
if (symbol is IMethodSymbol)
return false;
// However, many names look like expressions. For example:
// foreach (var |
// ('var' is a TypeSyntax which is an expression syntax.
var type = token.GetAncestors<TypeSyntax>().LastOrDefault();
if (type == null)
return true;
if (type.Kind() is SyntaxKind.GenericName or SyntaxKind.AliasQualifiedName or SyntaxKind.PredefinedType)
return false;
ExpressionSyntax nameExpr = type;
if (IsRightSideName(nameExpr))
{
nameExpr = (ExpressionSyntax)nameExpr.Parent!;
}
// If this name is the start of a local variable declaration context, we
// shouldn't show is or as. For example: for(var |
if (syntaxTree.IsLocalVariableDeclarationContext(token.SpanStart, syntaxTree.FindTokenOnLeftOfPosition(token.SpanStart, cancellationToken), cancellationToken))
return false;
// Not on the left hand side of an object initializer
if (token.IsKind(SyntaxKind.IdentifierToken) &&
token.Parent.IsKind(SyntaxKind.IdentifierName) &&
token.Parent.Parent is (kind: SyntaxKind.ObjectInitializerExpression or SyntaxKind.CollectionInitializerExpression))
{
return false;
}
// Not after an 'out' declaration expression. For example: M(out var |
if (token.Kind() is SyntaxKind.IdentifierToken &&
token.Parent.IsKind(SyntaxKind.IdentifierName))
{
if (token.Parent.Parent is ArgumentSyntax { RefOrOutKeyword.RawKind: (int)SyntaxKind.OutKeyword })
return false;
}
if (token.Text == SyntaxFacts.GetText(SyntaxKind.AsyncKeyword))
{
// async $$
//
// 'async' will look like a normal identifier. But we don't want to follow it
// with 'is' or 'as' or 'with' if it's actually the start of a lambda.
var delegateType = CSharpTypeInferenceService.Instance.InferDelegateType(
semanticModel, token.SpanStart, cancellationToken);
if (delegateType != null)
{
return false;
}
}
// case X $$
//
// while `X` is in an expr context, it's a limited one that doesn't support the full breadth of operators like these.
var tokenBeforeName = syntaxTree.FindTokenOnLeftOfPosition(nameExpr.SpanStart, cancellationToken);
if (tokenBeforeName.Kind() == SyntaxKind.CaseKeyword)
return false;
// Now, make sure the name was actually in a location valid for
// an expression. If so, then we know we can follow it.
if (syntaxTree.IsExpressionContext(nameExpr.SpanStart, tokenBeforeName, attributes: false, cancellationToken))
return true;
}
return false;
}
private static bool IsRightSideName(ExpressionSyntax name)
{
if (name.Parent != null)
{
switch (name.Parent.Kind())
{
case SyntaxKind.QualifiedName:
return ((QualifiedNameSyntax)name.Parent).Right == name;
case SyntaxKind.AliasQualifiedName:
return ((AliasQualifiedNameSyntax)name.Parent).Name == name;
case SyntaxKind.SimpleMemberAccessExpression:
return ((MemberAccessExpressionSyntax)name.Parent).Name == name;
}
}
return false;
}
public static bool IsCatchOrFinallyContext(
this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
// cases:
// try {
// } |
// try {
// } c|
// try {
// } catch {
// } |
// try {
// } catch {
// } c|
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (token.IsKind(SyntaxKind.CloseBraceToken))
{
var block = token.GetAncestor<BlockSyntax>();
if (block != null &&
token == block.GetLastToken(includeSkipped: true) &&
block.Parent?.Kind() is SyntaxKind.TryStatement or SyntaxKind.CatchClause)
{
return true;
}
}
return false;
}
public static bool IsCatchFilterContext(this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition)
{
// cases:
// catch |
// catch i|
// catch (declaration) |
// catch (declaration) i|
var token = tokenOnLeftOfPosition;
token = token.GetPreviousTokenIfTouchingWord(position);
if (CodeAnalysis.CSharpExtensions.IsKind(token, SyntaxKind.CatchKeyword))
{
return true;
}
if (CodeAnalysis.CSharpExtensions.IsKind(token, SyntaxKind.CloseParenToken) &&
token.Parent.IsKind(SyntaxKind.CatchDeclaration))
{
return true;
}
return false;
}
public static bool IsEnumBaseListContext(this SyntaxTree syntaxTree, SyntaxToken targetToken)
{
// Options:
// enum E : |
// enum E : i|
return
targetToken.IsKind(SyntaxKind.ColonToken) &&
targetToken.Parent.IsKind(SyntaxKind.BaseList) &&
targetToken.Parent.IsParentKind(SyntaxKind.EnumDeclaration);
}
public static bool IsEnumTypeMemberAccessContext(this SyntaxTree syntaxTree, SemanticModel semanticModel, int position, CancellationToken cancellationToken)
{
var token = syntaxTree
.FindTokenOnLeftOfPosition(position, cancellationToken)
.GetPreviousTokenIfTouchingWord(position);
if (!token.IsKind(SyntaxKind.DotToken))
{
return false;
}
SymbolInfo leftHandBinding;
if (token.Parent.IsKind(SyntaxKind.SimpleMemberAccessExpression))
{
var memberAccess = (MemberAccessExpressionSyntax)token.Parent;
leftHandBinding = semanticModel.GetSymbolInfo(memberAccess.Expression, cancellationToken);
}
else if (token.Parent is QualifiedNameSyntax qualifiedName &&
token.Parent?.Parent is BinaryExpressionSyntax(SyntaxKind.IsExpression) binaryExpression &&
binaryExpression.Right == qualifiedName)
{
// The right-hand side of an is expression could be an enum
leftHandBinding = semanticModel.GetSymbolInfo(qualifiedName.Left, cancellationToken);
}
else if (token.Parent is QualifiedNameSyntax qualifiedName1 &&
token.Parent?.Parent is DeclarationPatternSyntax declarationExpression &&
declarationExpression.Type == qualifiedName1)
{
// The right-hand side of an is declaration expression could be an enum
leftHandBinding = semanticModel.GetSymbolInfo(qualifiedName1.Left, cancellationToken);
}
else
{
return false;
}
var symbol = leftHandBinding.GetBestOrAllSymbols().FirstOrDefault();
if (symbol == null)
{
return false;
}
switch (symbol.Kind)
{
case SymbolKind.NamedType:
return ((INamedTypeSymbol)symbol).TypeKind == TypeKind.Enum;
case SymbolKind.Alias:
var target = ((IAliasSymbol)symbol).Target;
return target.IsType && ((ITypeSymbol)target).TypeKind == TypeKind.Enum;
}
return false;
}
public static bool IsFunctionPointerCallingConventionContext(this SyntaxTree syntaxTree, SyntaxToken targetToken)
{
return targetToken.IsKind(SyntaxKind.AsteriskToken) &&
targetToken.Parent is FunctionPointerTypeSyntax functionPointerType &&
targetToken == functionPointerType.AsteriskToken;
}
}
|