| File: VirtualizeItemComparerAnalyzer.cs | Web Access |
| Project: src\aspnetcore\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.RegisterOperationBlockAction(blockContext => { var componentStack = new Stack<ComponentState>(); var completedVirtualizeComponents = new List<ComponentState>(); foreach (var operationBlock in blockContext.OperationBlocks) { foreach (var operation in operationBlock.DescendantsAndSelf()) { if (operation is not IInvocationOperation invocation) { continue; } var targetMethod = invocation.TargetMethod; if (!SymbolEqualityComparer.Default.Equals(targetMethod.ContainingType, renderTreeBuilderType)) { continue; } 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; } } } foreach (var state in completedVirtualizeComponents) { if (state.HasItemsProvider && !state.HasItemComparer && state.ItemsProviderLocation is not null) { blockContext.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; } } }