| File: Classification\ClassificationHelpers.cs | Web Access |
| Project: src\roslyn\src\Workspaces\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Workspaces.csproj (Microsoft.CodeAnalysis.CSharp.Workspaces) |
// 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.Linq; 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; using Roslyn.Utilities; 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()); public 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; } } public 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) { if (methodDeclaration.ParameterList.Parameters is [var parameter, ..] && parameter.Modifiers.Any(SyntaxKind.ThisKeyword)) return ClassificationTypeNames.ExtensionMethodName; if (methodDeclaration.Parent is ExtensionBlockDeclarationSyntax) return ClassificationTypeNames.ExtensionMethodName; return 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, // Tracked by https://github.com/dotnet/roslyn/issues/82607 // Consider using a separate classification type for unions so users can color them differently SyntaxKind.UnionDeclaration => 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 string? GetClassificationForTypeDeclarationIdentifier(SyntaxToken identifier) => identifier.Parent!.Kind() switch { SyntaxKind.ClassDeclaration => ClassificationTypeNames.ClassName, SyntaxKind.EnumDeclaration => ClassificationTypeNames.EnumName, SyntaxKind.StructDeclaration => ClassificationTypeNames.StructName, SyntaxKind.UnionDeclaration => 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; } /// <summary> /// Determines if the given XML element is a code block with C# language attribute. /// </summary> public static (bool isCSharp, bool isCSharpTest) IsCodeBlockWithCSharpLang(XmlElementSyntax node) { if (node.StartTag.Name.LocalName.Text == DocumentationCommentXmlNames.CodeElementName) { foreach (var attribute in node.StartTag.Attributes) { if (attribute is XmlTextAttributeSyntax { Name.LocalName.Text: "lang" } textAttribute) { var langValue = string.Join("", textAttribute.TextTokens.Select(t => t.Text)).ToLower(); return (langValue is "c#", langValue is "c#-test"); } } } return default; } }