|
// 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.Threading;
using Microsoft.CodeAnalysis.Classification;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace Microsoft.CodeAnalysis.CSharp.Classification;
internal static class ClassificationHelpers
{
private const string FromKeyword = "from";
private const string VarKeyword = "var";
private const string UnmanagedKeyword = "unmanaged";
private const string NotNullKeyword = "notnull";
private const string DynamicKeyword = "dynamic";
private const string AwaitKeyword = "await";
/// <summary>
/// Determine the classification type for a given token.
/// </summary>
/// <param name="token">The token.</param>
/// <returns>The correct syntactic classification for the token.</returns>
public static string? GetClassification(SyntaxToken token)
{
if (IsControlKeyword(token))
{
return ClassificationTypeNames.ControlKeyword;
}
else if (SyntaxFacts.IsKeywordKind(token.Kind()) || token.IsKind(SyntaxKind.DiscardDesignation))
{
// When classifying `_`, IsKeywordKind handles UnderscoreToken, but need to additional check for DiscardDesignation
return ClassificationTypeNames.Keyword;
}
else if (SyntaxFacts.IsPunctuation(token.Kind()))
{
return GetClassificationForPunctuation(token);
}
else if (token.Kind() == SyntaxKind.IdentifierToken)
{
return GetSyntacticClassificationForIdentifier(token);
}
else if (IsStringToken(token))
{
return IsVerbatimStringToken(token)
? ClassificationTypeNames.VerbatimStringLiteral
: ClassificationTypeNames.StringLiteral;
}
else if (token.Kind() == SyntaxKind.NumericLiteralToken)
{
return ClassificationTypeNames.NumericLiteral;
}
return null;
}
private static bool IsControlKeyword(SyntaxToken token)
=> token.Parent is not null &&
IsControlKeywordKind(token.Kind()) &&
IsControlStatementKind(token.Parent.Kind());
private static bool IsControlKeywordKind(SyntaxKind kind)
{
switch (kind)
{
case SyntaxKind.AwaitKeyword:
case SyntaxKind.BreakKeyword:
case SyntaxKind.CaseKeyword:
case SyntaxKind.CatchKeyword:
case SyntaxKind.ContinueKeyword:
case SyntaxKind.DefaultKeyword: // Include DefaultKeyword as it can be part of a DefaultSwitchLabel
case SyntaxKind.DoKeyword:
case SyntaxKind.ElseKeyword:
case SyntaxKind.FinallyKeyword:
case SyntaxKind.ForEachKeyword:
case SyntaxKind.ForKeyword:
case SyntaxKind.GotoKeyword:
case SyntaxKind.IfKeyword:
case SyntaxKind.InKeyword: // Include InKeyword as it can be part of an ForEachStatement
case SyntaxKind.ReturnKeyword:
case SyntaxKind.SwitchKeyword:
case SyntaxKind.ThrowKeyword:
case SyntaxKind.TryKeyword:
case SyntaxKind.WhenKeyword: // Include WhenKeyword as it can be part of a CatchFilterClause or a pattern WhenClause
case SyntaxKind.WhileKeyword:
case SyntaxKind.YieldKeyword:
return true;
default:
return false;
}
}
private static bool IsControlStatementKind(SyntaxKind kind)
{
switch (kind)
{
// Jump Statements
case SyntaxKind.BreakStatement:
case SyntaxKind.ContinueStatement:
case SyntaxKind.DoStatement:
case SyntaxKind.ForEachStatement:
case SyntaxKind.ForEachVariableStatement:
case SyntaxKind.ForStatement:
case SyntaxKind.GotoCaseStatement:
case SyntaxKind.GotoDefaultStatement:
case SyntaxKind.GotoStatement:
case SyntaxKind.ReturnStatement:
case SyntaxKind.ThrowStatement:
case SyntaxKind.WhileStatement:
case SyntaxKind.YieldBreakStatement:
case SyntaxKind.YieldReturnStatement:
// Checked Statements
case SyntaxKind.AwaitExpression:
case SyntaxKind.CasePatternSwitchLabel:
case SyntaxKind.CaseSwitchLabel:
case SyntaxKind.CatchClause:
case SyntaxKind.CatchFilterClause:
case SyntaxKind.DefaultSwitchLabel:
case SyntaxKind.ElseClause:
case SyntaxKind.FinallyClause:
case SyntaxKind.IfStatement:
case SyntaxKind.SwitchExpression:
case SyntaxKind.SwitchSection:
case SyntaxKind.SwitchStatement:
case SyntaxKind.ThrowExpression:
case SyntaxKind.TryStatement:
case SyntaxKind.WhenClause:
return true;
default:
return false;
}
}
private static bool IsStringToken(SyntaxToken token)
{
return token.Kind()
is SyntaxKind.StringLiteralToken
or SyntaxKind.Utf8StringLiteralToken
or SyntaxKind.CharacterLiteralToken
or SyntaxKind.InterpolatedStringStartToken
or SyntaxKind.InterpolatedVerbatimStringStartToken
or SyntaxKind.InterpolatedStringTextToken
or SyntaxKind.InterpolatedStringEndToken
or SyntaxKind.InterpolatedRawStringEndToken
or SyntaxKind.InterpolatedSingleLineRawStringStartToken
or SyntaxKind.InterpolatedMultiLineRawStringStartToken
or SyntaxKind.SingleLineRawStringLiteralToken
or SyntaxKind.Utf8SingleLineRawStringLiteralToken
or SyntaxKind.MultiLineRawStringLiteralToken
or SyntaxKind.Utf8MultiLineRawStringLiteralToken;
}
private static bool IsVerbatimStringToken(SyntaxToken token)
{
if (token.IsVerbatimStringLiteral())
{
return true;
}
switch (token.Kind())
{
case SyntaxKind.InterpolatedVerbatimStringStartToken:
return true;
case SyntaxKind.InterpolatedStringStartToken:
return false;
case SyntaxKind.InterpolatedStringEndToken:
{
return token.Parent is InterpolatedStringExpressionSyntax interpolatedString
&& interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken);
}
case SyntaxKind.InterpolatedStringTextToken:
{
if (token.Parent is not InterpolatedStringTextSyntax interpolatedStringText)
{
return false;
}
return interpolatedStringText.Parent is InterpolatedStringExpressionSyntax interpolatedString
&& interpolatedString.StringStartToken.IsKind(SyntaxKind.InterpolatedVerbatimStringStartToken);
}
}
return false;
}
public static string? GetSyntacticClassificationForIdentifier(SyntaxToken token)
{
if (token.Parent is BaseTypeDeclarationSyntax typeDeclaration && typeDeclaration.Identifier == token)
{
return GetClassificationForTypeDeclarationIdentifier(token);
}
else if (token.Parent is DelegateDeclarationSyntax delegateDecl && delegateDecl.Identifier == token)
{
return ClassificationTypeNames.DelegateName;
}
else if (token.Parent is TypeParameterSyntax typeParameter && typeParameter.Identifier == token)
{
return ClassificationTypeNames.TypeParameterName;
}
else if (token.Parent is MethodDeclarationSyntax methodDeclaration && methodDeclaration.Identifier == token)
{
return IsExtensionMethod(methodDeclaration) ? ClassificationTypeNames.ExtensionMethodName : ClassificationTypeNames.MethodName;
}
else if (token.Parent is ConstructorDeclarationSyntax constructorDeclaration && constructorDeclaration.Identifier == token)
{
return GetClassificationTypeForConstructorOrDestructorParent(constructorDeclaration.Parent!);
}
else if (token.Parent is DestructorDeclarationSyntax destructorDeclaration && destructorDeclaration.Identifier == token)
{
return GetClassificationTypeForConstructorOrDestructorParent(destructorDeclaration.Parent!);
}
else if (token.Parent is LocalFunctionStatementSyntax localFunctionStatement && localFunctionStatement.Identifier == token)
{
return ClassificationTypeNames.MethodName;
}
else if (token.Parent is PropertyDeclarationSyntax propertyDeclaration && propertyDeclaration.Identifier == token)
{
return ClassificationTypeNames.PropertyName;
}
else if (token.Parent is EnumMemberDeclarationSyntax enumMemberDeclaration && enumMemberDeclaration.Identifier == token)
{
return ClassificationTypeNames.EnumMemberName;
}
else if (token.Parent is CatchDeclarationSyntax catchDeclaration && catchDeclaration.Identifier == token)
{
return ClassificationTypeNames.LocalName;
}
else if (token.Parent is VariableDeclaratorSyntax variableDeclarator && variableDeclarator.Identifier == token)
{
var varDecl = variableDeclarator.Parent as VariableDeclarationSyntax;
return varDecl?.Parent switch
{
FieldDeclarationSyntax fieldDeclaration => fieldDeclaration.Modifiers.Any(SyntaxKind.ConstKeyword) ? ClassificationTypeNames.ConstantName : ClassificationTypeNames.FieldName,
LocalDeclarationStatementSyntax localDeclarationStatement => localDeclarationStatement.IsConst ? ClassificationTypeNames.ConstantName : ClassificationTypeNames.LocalName,
EventFieldDeclarationSyntax _ => ClassificationTypeNames.EventName,
_ => ClassificationTypeNames.LocalName,
};
}
else if (token.Parent is SingleVariableDesignationSyntax singleVariableDesignation && singleVariableDesignation.Identifier == token)
{
return ClassificationTypeNames.LocalName;
}
else if (token.Parent is ParameterSyntax parameterSyntax && parameterSyntax.Identifier == token)
{
return ClassificationTypeNames.ParameterName;
}
else if (token.Parent is ForEachStatementSyntax forEachStatementSyntax && forEachStatementSyntax.Identifier == token)
{
return ClassificationTypeNames.LocalName;
}
else if (token.Parent is EventDeclarationSyntax eventDeclarationSyntax && eventDeclarationSyntax.Identifier == token)
{
return ClassificationTypeNames.EventName;
}
else if (IsActualContextualKeyword(token))
{
return ClassificationTypeNames.Keyword;
}
else if (token.Parent is IdentifierNameSyntax identifierNameSyntax && IsNamespaceName(identifierNameSyntax))
{
return ClassificationTypeNames.NamespaceName;
}
else if (token.Parent is ExternAliasDirectiveSyntax externAliasDirectiveSyntax && externAliasDirectiveSyntax.Identifier == token)
{
return ClassificationTypeNames.NamespaceName;
}
else if (token.Parent is LabeledStatementSyntax labledStatementSyntax && labledStatementSyntax.Identifier == token)
{
return ClassificationTypeNames.LabelName;
}
else
{
return ClassificationTypeNames.Identifier;
}
}
private static string? GetClassificationTypeForConstructorOrDestructorParent(SyntaxNode parentNode)
=> parentNode.Kind() switch
{
SyntaxKind.ClassDeclaration => ClassificationTypeNames.ClassName,
SyntaxKind.InterfaceDeclaration => ClassificationTypeNames.InterfaceName,
SyntaxKind.RecordDeclaration => ClassificationTypeNames.RecordClassName,
SyntaxKind.RecordStructDeclaration => ClassificationTypeNames.RecordStructName,
SyntaxKind.StructDeclaration => ClassificationTypeNames.StructName,
_ => null
};
private static bool IsNamespaceName(IdentifierNameSyntax identifierSyntax)
{
var parent = identifierSyntax.Parent;
while (parent is QualifiedNameSyntax)
parent = parent.Parent;
return parent is BaseNamespaceDeclarationSyntax;
}
public static bool IsStaticallyDeclared(SyntaxToken token)
{
var parentNode = token.Parent;
if (parentNode.IsKind(SyntaxKind.EnumMemberDeclaration))
{
// EnumMembers are not classified as static since there is no
// instance equivalent of the concept and they have their own
// classification type.
return false;
}
else if (parentNode.IsKind(SyntaxKind.VariableDeclarator))
{
// The parent of a VariableDeclarator is a VariableDeclarationSyntax node.
// It's parent will be the declaration syntax node.
parentNode = parentNode!.Parent!.Parent;
// Check if this is a field constant declaration
if (parentNode.GetModifiers().Any(SyntaxKind.ConstKeyword))
{
return true;
}
}
return parentNode.GetModifiers().Any(SyntaxKind.StaticKeyword);
}
private static bool IsExtensionMethod(MethodDeclarationSyntax methodDeclaration)
=> methodDeclaration.ParameterList.Parameters.FirstOrDefault()?.Modifiers.Any(SyntaxKind.ThisKeyword) == true;
private static string? GetClassificationForTypeDeclarationIdentifier(SyntaxToken identifier)
=> identifier.Parent!.Kind() switch
{
SyntaxKind.ClassDeclaration => ClassificationTypeNames.ClassName,
SyntaxKind.EnumDeclaration => ClassificationTypeNames.EnumName,
SyntaxKind.StructDeclaration => ClassificationTypeNames.StructName,
SyntaxKind.InterfaceDeclaration => ClassificationTypeNames.InterfaceName,
SyntaxKind.RecordDeclaration => ClassificationTypeNames.RecordClassName,
SyntaxKind.RecordStructDeclaration => ClassificationTypeNames.RecordStructName,
_ => null,
};
private static string GetClassificationForPunctuation(SyntaxToken token)
{
if (token.Kind().IsOperator())
{
// special cases...
switch (token.Kind())
{
case SyntaxKind.LessThanToken:
case SyntaxKind.GreaterThanToken:
// the < and > tokens of a type parameter list or function pointer parameter
// list should be classified as punctuation; otherwise, they're operators.
if (token.Parent != null)
{
if (token.Parent.Kind() is SyntaxKind.TypeParameterList or
SyntaxKind.TypeArgumentList or
SyntaxKind.FunctionPointerParameterList)
{
return ClassificationTypeNames.Punctuation;
}
}
break;
case SyntaxKind.ColonToken:
// the : for inheritance/implements or labels should be classified as
// punctuation; otherwise, it's from a conditional operator.
if (token.Parent != null)
{
if (token.Parent.Kind() != SyntaxKind.ConditionalExpression)
{
return ClassificationTypeNames.Punctuation;
}
}
break;
}
return ClassificationTypeNames.Operator;
}
else
{
return ClassificationTypeNames.Punctuation;
}
}
private static bool IsOperator(this SyntaxKind kind)
{
switch (kind)
{
case SyntaxKind.TildeToken:
case SyntaxKind.ExclamationToken:
case SyntaxKind.PercentToken:
case SyntaxKind.CaretToken:
case SyntaxKind.AmpersandToken:
case SyntaxKind.AsteriskToken:
case SyntaxKind.MinusToken:
case SyntaxKind.PlusToken:
case SyntaxKind.EqualsToken:
case SyntaxKind.BarToken:
case SyntaxKind.ColonToken:
case SyntaxKind.LessThanToken:
case SyntaxKind.GreaterThanToken:
case SyntaxKind.DotToken:
case SyntaxKind.QuestionToken:
case SyntaxKind.SlashToken:
case SyntaxKind.BarBarToken:
case SyntaxKind.AmpersandAmpersandToken:
case SyntaxKind.MinusMinusToken:
case SyntaxKind.PlusPlusToken:
case SyntaxKind.ColonColonToken:
case SyntaxKind.QuestionQuestionToken:
case SyntaxKind.MinusGreaterThanToken:
case SyntaxKind.ExclamationEqualsToken:
case SyntaxKind.EqualsEqualsToken:
case SyntaxKind.EqualsGreaterThanToken:
case SyntaxKind.LessThanEqualsToken:
case SyntaxKind.LessThanLessThanToken:
case SyntaxKind.LessThanLessThanEqualsToken:
case SyntaxKind.GreaterThanEqualsToken:
case SyntaxKind.GreaterThanGreaterThanToken:
case SyntaxKind.GreaterThanGreaterThanGreaterThanToken:
case SyntaxKind.GreaterThanGreaterThanEqualsToken:
case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken:
case SyntaxKind.SlashEqualsToken:
case SyntaxKind.AsteriskEqualsToken:
case SyntaxKind.BarEqualsToken:
case SyntaxKind.AmpersandEqualsToken:
case SyntaxKind.PlusEqualsToken:
case SyntaxKind.MinusEqualsToken:
case SyntaxKind.CaretEqualsToken:
case SyntaxKind.PercentEqualsToken:
case SyntaxKind.QuestionQuestionEqualsToken:
return true;
default:
return false;
}
}
private static bool IsActualContextualKeyword(SyntaxToken token)
{
if (token.Parent is LabeledStatementSyntax statement &&
statement.Identifier == token)
{
return false;
}
// Ensure that the text and value text are the same. Otherwise, the identifier might
// be escaped. I.e. "var", but not "@var"
if (token.ToString() != token.ValueText)
{
return false;
}
// Standard cases. We can just check the parent and see if we're
// in the right position to be considered a contextual keyword
if (token.Parent != null)
{
switch (token.ValueText)
{
case AwaitKeyword:
return token.GetNextToken(includeZeroWidth: true).IsMissing;
case FromKeyword:
var fromClause = token.Parent.FirstAncestorOrSelf<FromClauseSyntax>();
return fromClause != null && fromClause.FromKeyword == token;
case VarKeyword:
// var
if (token.Parent is IdentifierNameSyntax && token.Parent?.Parent is ExpressionStatementSyntax)
{
return true;
}
// we allow var any time it looks like a variable declaration, and is not in a
// field or event field.
return
token.Parent is IdentifierNameSyntax &&
token.Parent.Parent is VariableDeclarationSyntax &&
!(token.Parent.Parent.Parent is FieldDeclarationSyntax) &&
!(token.Parent.Parent.Parent is EventFieldDeclarationSyntax);
case UnmanagedKeyword:
case NotNullKeyword:
return token.Parent is IdentifierNameSyntax
&& token.Parent.Parent is TypeConstraintSyntax
&& token.Parent.Parent.Parent is TypeParameterConstraintClauseSyntax;
}
}
return false;
}
internal static void AddLexicalClassifications(SourceText text, TextSpan textSpan, SegmentedList<ClassifiedSpan> result, CancellationToken cancellationToken)
{
var text2 = text.ToString(textSpan);
var tokens = SyntaxFactory.ParseTokens(text2, initialTokenPosition: textSpan.Start);
Worker.CollectClassifiedSpans(tokens, textSpan, result, cancellationToken);
}
internal static ClassifiedSpan AdjustStaleClassification(SourceText rawText, ClassifiedSpan classifiedSpan)
{
// If we marked this as an identifier and it should now be a keyword
// (or vice versa), then fix this up and return it.
var classificationType = classifiedSpan.ClassificationType;
// Check if the token's type has changed. Note: we don't check for "wasPPKeyword &&
// !isPPKeyword" here. That's because for fault tolerance any identifier will end up
// being parsed as a PP keyword eventually, and if we have the check here, the text
// flickers between blue and black while typing. See
// http://vstfdevdiv:8080/web/wi.aspx?id=3521 for details.
var wasKeyword = classificationType == ClassificationTypeNames.Keyword;
var wasIdentifier = classificationType == ClassificationTypeNames.Identifier;
// We only do this for identifiers/keywords.
if (wasKeyword || wasIdentifier)
{
// Get the current text under the tag.
var span = classifiedSpan.TextSpan;
var text = rawText.ToString(span);
// Now, try to find the token that corresponds to that text. If
// we get 0 or 2+ tokens, then we can't do anything with this.
// Also, if that text includes trivia, then we can't do anything.
var token = SyntaxFactory.ParseToken(text);
if (token.Span.Length == span.Length)
{
// var, dynamic, and unmanaged are not contextual keywords. They are always identifiers
// (that we classify as keywords). Because we are just parsing a token we don't
// know if we're in the right context for them to be identifiers or keywords.
// So, we base on decision on what they were before. i.e. if we had a keyword
// before, then assume it stays a keyword if we see 'var', 'dynamic', or 'unmanaged'.
var tokenString = token.ToString();
var isKeyword = SyntaxFacts.IsKeywordKind(token.Kind())
|| (wasKeyword && SyntaxFacts.GetContextualKeywordKind(text) != SyntaxKind.None)
|| (wasKeyword && (tokenString == VarKeyword || tokenString == DynamicKeyword || tokenString == UnmanagedKeyword || tokenString == NotNullKeyword));
var isIdentifier = token.Kind() == SyntaxKind.IdentifierToken;
// We only do this for identifiers/keywords.
if (isKeyword || isIdentifier)
{
if ((wasKeyword && !isKeyword) ||
(wasIdentifier && !isIdentifier))
{
// It changed! Return the new type of tagspan.
return new ClassifiedSpan(
isKeyword ? ClassificationTypeNames.Keyword : ClassificationTypeNames.Identifier, span);
}
}
}
}
// didn't need to do anything to this one.
return classifiedSpan;
}
}
|