File: EmbeddedLanguages\DateAndTime\LanguageServices\DateAndTimeLanguageDetector.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.Features)
// 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.Diagnostics;
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.DateAndTime.LanguageServices;
 
/// <summary>
/// Helper class to detect <see cref="DateTime"/> and <see cref="DateTimeOffset"/> format strings in a document efficiently.
/// </summary>
internal sealed class DateAndTimeLanguageDetector(
    EmbeddedLanguageInfo info,
    Compilation compilation)
    : AbstractLanguageDetector<DateAndTimeOptions, DateTimeTree, DateAndTimeLanguageDetector, DateAndTimeLanguageDetector.DateAndTimeInfo>(
        info, LanguageIdentifiers, CommentDetector)
{
    internal readonly struct DateAndTimeInfo : ILanguageDetectorInfo<DateAndTimeLanguageDetector>
    {
        public ImmutableArray<string> LanguageIdentifiers => ["Date", "Time", "DateTime", "DateTimeFormat"];
 
        public DateAndTimeLanguageDetector Create(Compilation compilation, EmbeddedLanguageInfo info)
            => new DateAndTimeLanguageDetector(info, compilation);
    }
 
    private const string FormatName = "format";
 
    private readonly Compilation _compilation = compilation;
    private INamedTypeSymbol? _dateTimeType;
    private INamedTypeSymbol? _dateTimeOffsetType;
 
    protected override bool TryGetOptions(SemanticModel semanticModel, ITypeSymbol exprType, SyntaxNode expr, CancellationToken cancellationToken, out DateAndTimeOptions options)
    {
        // DateTime never has any options.  So just return empty and 'true' so we stop processing immediately.
        options = default;
        return true;
    }
 
    protected override DateTimeTree? TryParse(VirtualCharSequence chars, DateAndTimeOptions options)
    {
        // Once we've determined something is a DateTime string, then parsing is a no-op.  We just return a dummy
        // instance to satisfy the detector requirements.
        return DateTimeTree.Instance;
    }
 
    protected override bool IsEmbeddedLanguageInterpolatedStringTextToken(SyntaxToken token, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        var syntaxFacts = Info.SyntaxFacts;
        var interpolationFormatClause = token.Parent;
        var interpolation = interpolationFormatClause?.Parent;
        if (interpolation?.RawKind != syntaxFacts.SyntaxKinds.Interpolation)
            return false;
 
        var expression = syntaxFacts.GetExpressionOfInterpolation(interpolation);
        var type = semanticModel.GetTypeInfo(expression, cancellationToken).Type;
        return IsDateTimeType(type);
    }
 
    protected override bool IsArgumentToWellKnownAPI(
        SyntaxToken token,
        SyntaxNode argumentNode,
        SemanticModel semanticModel,
        CancellationToken cancellationToken,
        out DateAndTimeOptions options)
    {
        options = default;
 
        var argumentList = argumentNode.Parent;
        var invocationOrCreation = argumentList?.Parent;
 
        var syntaxFacts = Info.SyntaxFacts;
        if (!syntaxFacts.IsInvocationExpression(invocationOrCreation))
            return false;
 
        var invokedExpression = syntaxFacts.GetExpressionOfInvocationExpression(invocationOrCreation);
        var name = GetNameOfInvokedExpression(syntaxFacts, invokedExpression);
        if (name is not nameof(ToString) and not nameof(DateTime.ParseExact) and not nameof(DateTime.TryParseExact))
            return false;
 
        // We have a string literal passed to a method called ToString/ParseExact/TryParseExact.
        // Have to do a more expensive semantic check now.
 
        // if we couldn't determine the arg name or arg index, can't proceed.
        var (argName, argIndex) = GetArgumentNameOrIndex(argumentNode);
        if (argName == null && argIndex == null)
            return false;
 
        // If we had a specified arg name and it isn't 'format', then it's not a DateTime
        // 'format' param we care about.
        if (argName is not null and not FormatName)
            return false;
 
        var symbolInfo = semanticModel.GetSymbolInfo(invocationOrCreation, cancellationToken);
        var method = symbolInfo.Symbol;
        if (TryAnalyzeInvocation(method, argName, argIndex))
            return true;
 
        foreach (var candidate in symbolInfo.CandidateSymbols)
        {
            if (TryAnalyzeInvocation(candidate, argName, argIndex))
                return true;
        }
 
        return false;
    }
 
    private static string? GetNameOfInvokedExpression(ISyntaxFacts syntaxFacts, SyntaxNode invokedExpression)
    {
        if (syntaxFacts.IsSimpleMemberAccessExpression(invokedExpression))
            return syntaxFacts.GetIdentifierOfSimpleName(syntaxFacts.GetNameOfMemberAccessExpression(invokedExpression)).ValueText;
 
        if (syntaxFacts.IsMemberBindingExpression(invokedExpression))
            invokedExpression = syntaxFacts.GetNameOfMemberBindingExpression(invokedExpression);
 
        if (syntaxFacts.IsIdentifierName(invokedExpression))
            return syntaxFacts.GetIdentifierOfSimpleName(invokedExpression).ValueText;
 
        return null;
    }
 
    private static bool IsMethodArgument(SyntaxToken token, ISyntaxFacts syntaxFacts)
        => syntaxFacts.IsLiteralExpression(token.Parent) &&
           syntaxFacts.IsArgument(token.Parent!.Parent);
 
    private (string? name, int? index) GetArgumentNameOrIndex(SyntaxNode argument)
    {
        var syntaxFacts = Info.SyntaxFacts;
 
        var argName = syntaxFacts.GetNameForArgument(argument);
        if (argName != "")
            return (argName, null);
 
        var arguments = syntaxFacts.GetArgumentsOfArgumentList(argument.GetRequiredParent());
        var index = arguments.IndexOf(argument);
        if (index >= 0)
            return (null, index);
 
        return default;
    }
 
    private bool TryAnalyzeInvocation(ISymbol? symbol, string? argName, int? argIndex)
        => symbol is IMethodSymbol method &&
           method.DeclaredAccessibility == Accessibility.Public &&
           method.MethodKind == MethodKind.Ordinary &&
           IsDateTimeType(method.ContainingType) &&
           AnalyzeStringLiteral(method, argName, argIndex);
 
    private bool IsDateTimeType(ITypeSymbol? type)
    {
        if (type == null)
            return false;
 
        _dateTimeType ??= _compilation.GetTypeByMetadataName(typeof(DateTime).FullName!);
        _dateTimeOffsetType ??= _compilation.GetTypeByMetadataName(typeof(DateTimeOffset).FullName!);
 
        return type.Equals(_dateTimeType) || type.Equals(_dateTimeOffsetType);
    }
 
    private static bool AnalyzeStringLiteral(IMethodSymbol method, string? argName, int? argIndex)
    {
        Debug.Assert(argName != null || argIndex != null);
 
        var parameters = method.Parameters;
        if (argName != null)
            return parameters.Any(static (p, argName) => p.Name == argName, argName);
 
        var parameter = argIndex < parameters.Length ? parameters[argIndex.Value] : null;
        return parameter?.Name == FormatName;
    }
}