|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Immutable;
using System.Threading;
using Microsoft.CodeAnalysis.EmbeddedLanguages;
using Microsoft.CodeAnalysis.EmbeddedLanguages.VirtualChars;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Shared.Extensions;
namespace Microsoft.CodeAnalysis.Features.EmbeddedLanguages
{
internal abstract class AbstractLanguageDetector<TOptions>
where TOptions : struct, Enum
{
protected readonly EmbeddedLanguageInfo Info;
private readonly EmbeddedLanguageDetector _detector;
protected AbstractLanguageDetector(
EmbeddedLanguageInfo info,
ImmutableArray<string> languageIdentifiers)
{
Info = info;
_detector = new EmbeddedLanguageDetector(info, languageIdentifiers);
}
/// <summary>
/// Whether or not this is an argument to a well known api for this language (like Regex.Match or JToken.Parse).
/// We light up support if we detect these, even if these APIs don't have the StringSyntaxAttribute attribute on
/// them. That way users can get a decent experience even on downlevel frameworks.
/// </summary>
protected abstract bool IsArgumentToWellKnownAPI(SyntaxToken token, SyntaxNode argumentNode, SemanticModel semanticModel, CancellationToken cancellationToken, out TOptions options);
/// <summary>
/// Giving a sibling argument expression to the string literal, attempts to determine if they correspond to
/// options for that language. For example with <c>new Regex("[a-z]", RegexOptions.CaseInsensitive)</c> the
/// second argument's expression defines options that control how the literal is parsed.
/// </summary>
protected abstract bool TryGetOptions(SemanticModel semanticModel, ITypeSymbol exprType, SyntaxNode expr, CancellationToken cancellationToken, out TOptions options);
// Most embedded languages don't support being in an interpolated string text token.
protected virtual bool IsEmbeddedLanguageInterpolatedStringTextToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
=> false;
/// <summary>
/// What options we should assume by default if we're matched up against a symbol that has a [StringSyntax]
/// attribute on it.
/// </summary>
protected virtual TOptions GetStringSyntaxDefaultOptions()
=> default;
public bool IsEmbeddedLanguageToken(
SyntaxToken token,
SemanticModel semanticModel,
CancellationToken cancellationToken,
out TOptions options)
{
options = default;
// First check for the standard pattern of either a `// lang=...` comment or an API annotated with the
// [StringSyntax] attribute.
if (_detector.IsEmbeddedLanguageToken(token, semanticModel, cancellationToken, out _, out var stringOptions))
{
// If we got string-options back, then we were on a comment string (e.g. `// lang=regex,option1,option2`).
// Attempt to convert the string options to actual options requested.
if (stringOptions != null)
return EmbeddedLanguageCommentOptions<TOptions>.TryGetOptions(stringOptions, out options);
// If we weren't on a comment, then we were on an API with StringSyntaxAttribute on it. Attempt to grab
// API specific options for the client to use.
var syntaxFacts = Info.SyntaxFacts;
if (syntaxFacts.IsLiteralExpression(token.Parent) &&
syntaxFacts.IsArgument(token.Parent.Parent))
{
options = GetOptionsFromSiblingArgument(token.Parent.Parent, semanticModel, cancellationToken) ??
GetStringSyntaxDefaultOptions();
}
return true;
}
else
{
// We did not have a comment, and we didn't have a StringSyntax API. See if this is an unannotated API
// (e.g. Regex/Json prior to .net core).
if (Info.IsAnyStringLiteral(token.RawKind))
return IsEmbeddedLanguageStringLiteralToken(token, semanticModel, cancellationToken, out options);
if (token.RawKind == Info.SyntaxKinds.InterpolatedStringTextToken)
{
options = default;
return IsEmbeddedLanguageInterpolatedStringTextToken(token, semanticModel, cancellationToken);
}
return false;
}
}
private bool IsEmbeddedLanguageStringLiteralToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken, out TOptions options)
{
options = default;
var syntaxFacts = Info.SyntaxFacts;
return syntaxFacts.IsLiteralExpression(token.Parent) &&
syntaxFacts.IsArgument(token.Parent.Parent) &&
IsArgumentToWellKnownAPI(token, token.Parent.Parent, semanticModel, cancellationToken, out options);
}
protected TOptions? GetOptionsFromSiblingArgument(
SyntaxNode argument,
SemanticModel semanticModel,
CancellationToken cancellationToken)
{
var syntaxFacts = Info.SyntaxFacts;
var argumentList = argument.GetRequiredParent();
var arguments = syntaxFacts.IsArgument(argument)
? syntaxFacts.GetArgumentsOfArgumentList(argumentList)
: syntaxFacts.GetArgumentsOfAttributeArgumentList(argumentList);
foreach (var siblingArg in arguments)
{
if (siblingArg != argument)
{
var expr = syntaxFacts.IsArgument(argument)
? syntaxFacts.GetExpressionOfArgument(siblingArg)
: syntaxFacts.GetExpressionOfAttributeArgument(siblingArg);
if (expr != null)
{
var exprType = semanticModel.GetTypeInfo(expr, cancellationToken);
if (exprType.Type != null &&
TryGetOptions(semanticModel, exprType.Type, expr, cancellationToken, out var options))
{
return options;
}
}
}
}
return null;
}
protected static string? GetNameOfType(SyntaxNode? typeNode, ISyntaxFacts syntaxFacts)
{
if (syntaxFacts.IsQualifiedName(typeNode))
{
return GetNameOfType(syntaxFacts.GetRightSideOfDot(typeNode), syntaxFacts);
}
else if (syntaxFacts.IsIdentifierName(typeNode))
{
return syntaxFacts.GetIdentifierOfSimpleName(typeNode).ValueText;
}
return null;
}
protected string? GetNameOfInvokedExpression(SyntaxNode invokedExpression)
{
var syntaxFacts = Info.SyntaxFacts;
if (syntaxFacts.IsSimpleMemberAccessExpression(invokedExpression))
{
return syntaxFacts.GetIdentifierOfSimpleName(syntaxFacts.GetNameOfMemberAccessExpression(invokedExpression)).ValueText;
}
else if (syntaxFacts.IsIdentifierName(invokedExpression))
{
return syntaxFacts.GetIdentifierOfSimpleName(invokedExpression).ValueText;
}
return null;
}
}
internal abstract class AbstractLanguageDetector<TOptions, TTree> :
AbstractLanguageDetector<TOptions>
where TOptions : struct, Enum
where TTree : class
{
protected AbstractLanguageDetector(
EmbeddedLanguageInfo info,
ImmutableArray<string> languageIdentifiers)
: base(info, languageIdentifiers)
{
}
/// <summary>
/// Tries to parse out an appropriate language tree given the characters in this string literal.
/// </summary>
protected abstract TTree? TryParse(VirtualCharSequence chars, TOptions options);
/// <summary>
/// Attempts to parse the string-literal-like <paramref name="token"/> into an embedded language tree. The
/// token must either be in a location semantically known to accept this language, or it must have an
/// appropriate comment on it stating that it should be interpreted as this language.
/// </summary>
public TTree? TryParseString(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
{
if (!this.IsEmbeddedLanguageToken(token, semanticModel, cancellationToken, out var options))
return null;
var chars = Info.VirtualCharService.TryConvertToVirtualChars(token);
return TryParse(chars, options);
}
}
}
|