File: Diagnostics\TaskListDiagnosticProvider.cs
Web Access
Project: src\src\Razor\src\Razor\src\Microsoft.CodeAnalysis.Razor.Workspaces\Microsoft.CodeAnalysis.Razor.Workspaces.csproj (Microsoft.CodeAnalysis.Razor.Workspaces)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;
using Microsoft.AspNetCore.Razor.PooledObjects;
using Microsoft.CodeAnalysis.Razor.Protocol;
using Microsoft.CodeAnalysis.Text;
 
namespace Microsoft.CodeAnalysis.Razor.Diagnostics;
 
internal static class TaskListDiagnosticProvider
{
    private static readonly DiagnosticTag[] s_taskItemTags = [VSDiagnosticTags.TaskItem];
 
    public static ImmutableArray<LspDiagnostic> GetTaskListDiagnostics(RazorCodeDocument codeDocument, ImmutableArray<string> taskListDescriptors)
    {
        var source = codeDocument.Source.Text;
        var root = codeDocument.GetRequiredSyntaxRoot();
 
        using var diagnostics = new PooledArrayBuilder<LspDiagnostic>();
 
        foreach (var node in root.DescendantNodes())
        {
            if (node is RazorCommentBlockSyntax comment)
            {
                var i = comment.Comment.SpanStart;
 
                while (char.IsWhiteSpace(source[i]))
                {
                    i++;
                }
 
                foreach (var token in taskListDescriptors)
                {
                    if (!CommentMatchesToken(source, comment, i, token))
                    {
                        continue;
                    }
 
                    diagnostics.Add(new LspDiagnostic
                    {
                        Code = "TODO",
                        Message = comment.Comment.Content.Trim(),
                        Source = LanguageServerConstants.RazorDiagnosticSource,
                        Severity = LspDiagnosticSeverity.Information,
                        Range = source.GetRange(comment.Comment.Span),
                        Tags = s_taskItemTags
                    });
 
                    break;
                }
            }
        }
 
        return diagnostics.ToImmutable();
    }
 
    private static bool CommentMatchesToken(SourceText source, RazorCommentBlockSyntax comment, int i, string token)
    {
        if (i + token.Length + 2 > comment.EndCommentStar.SpanStart)
        {
            // Not enough room in the comment for the token and some content
            return false;
        }
 
        for (var j = 0; j < token.Length; j++)
        {
            if (source.Length < i + j)
            {
                return false;
            }
 
            if (char.ToLowerInvariant(source[i + j]) != char.ToLowerInvariant(token[j]))
            {
                return false;
            }
        }
 
        if (char.IsLetter(source[i + token.Length + 1]))
        {
            // The comment starts with the token, but the next character is a letter, which means it is something like "TODONT"
            return false;
        }
 
        return true;
    }
}