File: VirtualizeItemComparerAnalyzer.cs
Web Access
Project: src\src\Components\Analyzers\src\Microsoft.AspNetCore.Components.Analyzers.csproj (Microsoft.AspNetCore.Components.Analyzers)
// 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.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
 
#nullable enable
 
namespace Microsoft.AspNetCore.Components.Analyzers;
 
/// <summary>
/// Analyzer that detects usage of Virtualize with ItemsProvider but without ItemComparer.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class VirtualizeItemComparerAnalyzer : DiagnosticAnalyzer
{
    private const string VirtualizeTypeName = "Microsoft.AspNetCore.Components.Web.Virtualization.Virtualize`1";
    private const string RenderTreeBuilderTypeName = "Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder";
 
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(DiagnosticDescriptors.VirtualizeItemsProviderRequiresItemComparer);
 
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
 
        context.RegisterCompilationStartAction(compilationContext =>
        {
            var virtualizeType = compilationContext.Compilation.GetTypeByMetadataName(VirtualizeTypeName);
            var renderTreeBuilderType = compilationContext.Compilation.GetTypeByMetadataName(RenderTreeBuilderTypeName);
 
            if (virtualizeType is null || renderTreeBuilderType is null)
            {
                return;
            }
 
            compilationContext.RegisterOperationBlockStartAction(blockContext =>
            {
                var componentStack = new Stack<ComponentState>();
                var completedVirtualizeComponents = new List<ComponentState>();
 
                blockContext.RegisterOperationAction(operationContext =>
                {
                    var invocation = (IInvocationOperation)operationContext.Operation;
                    var targetMethod = invocation.TargetMethod;
 
                    if (!SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, renderTreeBuilderType))
                    {
                        return;
                    }
 
                    switch (targetMethod.Name)
                    {
                        case "OpenComponent":
                            if (targetMethod.IsGenericMethod && targetMethod.TypeArguments.Length == 1)
                            {
                                var typeArg = targetMethod.TypeArguments[0];
                                var originalDef = typeArg is INamedTypeSymbol namedType && namedType.IsGenericType
                                    ? namedType.OriginalDefinition
                                    : typeArg;
 
                                if (SymbolEqualityComparer.Default.Equals(originalDef, virtualizeType))
                                {
                                    componentStack.Push(new ComponentState { IsVirtualize = true });
                                }
                                else
                                {
                                    componentStack.Push(new ComponentState { IsVirtualize = false });
                                }
                            }
                            else
                            {
                                componentStack.Push(new ComponentState { IsVirtualize = false });
                            }
                            break;
 
                        case "AddComponentParameter":
                            if (componentStack.Count > 0 && componentStack.Peek().IsVirtualize)
                            {
                                if (invocation.Arguments.Length >= 2)
                                {
                                    var nameArg = invocation.Arguments[1];
                                    if (nameArg.Value.ConstantValue.HasValue &&
                                        nameArg.Value.ConstantValue.Value is string paramName)
                                    {
                                        var state = componentStack.Peek();
                                        if (paramName == "ItemsProvider")
                                        {
                                            state.HasItemsProvider = true;
                                            state.ItemsProviderLocation = invocation.Syntax.GetLocation();
                                        }
                                        else if (paramName == "ItemComparer")
                                        {
                                            state.HasItemComparer = true;
                                        }
                                    }
                                }
                            }
                            break;
 
                        case "CloseComponent":
                            if (componentStack.Count > 0)
                            {
                                var state = componentStack.Pop();
                                if (state.IsVirtualize)
                                {
                                    completedVirtualizeComponents.Add(state);
                                }
                            }
                            break;
                    }
                }, OperationKind.Invocation);
 
                blockContext.RegisterOperationBlockEndAction(endContext =>
                {
                    foreach (var state in completedVirtualizeComponents)
                    {
                        if (state.HasItemsProvider && !state.HasItemComparer && state.ItemsProviderLocation != null)
                        {
                            endContext.ReportDiagnostic(Diagnostic.Create(
                                DiagnosticDescriptors.VirtualizeItemsProviderRequiresItemComparer,
                                state.ItemsProviderLocation));
                        }
                    }
                });
            });
        });
    }
 
    private sealed class ComponentState
    {
        public bool IsVirtualize { get; set; }
        public bool HasItemsProvider { get; set; }
        public bool HasItemComparer { get; set; }
        public Location? ItemsProviderLocation { get; set; }
    }
}