File: ForLoopIteratorInClosureAnalyzer.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;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;
 
#nullable enable
 
namespace Microsoft.AspNetCore.Components.Analyzers;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ForLoopIteratorInClosureAnalyzer : DiagnosticAnalyzer
{
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        ImmutableArray.Create(DiagnosticDescriptors.ForLoopIteratorVariableUsedInClosure);
 
    public override void Initialize(AnalysisContext context)
    {
        context.EnableConcurrentExecution();
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);
 
        context.RegisterCompilationStartAction(compilationContext =>
        {
            var availableTypes = new Dictionary<string, INamedTypeSymbol?>()
            {
                {
                    ComponentsApi.BindConverter.MetadataName,
                    compilationContext.Compilation.GetTypeByMetadataName(ComponentsApi.BindConverter.FullTypeName)
                },
                {
                    ComponentsApi.ComponentBase.MetadataName,
                    compilationContext.Compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.FullTypeName)
                },
                {
                    ComponentsApi.EventCallbackFactory.MetadataName,
                    compilationContext.Compilation.GetTypeByMetadataName(ComponentsApi.EventCallbackFactory.FullTypeName)
                },
                {
                    ComponentsApi.RenderTreeBuilder.MetadataName,
                    compilationContext.Compilation.GetTypeByMetadataName(ComponentsApi.RenderTreeBuilder.FullTypeName)
                },
            };
            if (availableTypes[ComponentsApi.ComponentBase.MetadataName] is null
                || availableTypes[ComponentsApi.RenderTreeBuilder.MetadataName] is null)
            {
                return;
            }
 
            compilationContext.RegisterOperationBlockAction(blockContext =>
            {
                var analyzerState = new ForLoopAnalyzerState();
                if (blockContext.OwningSymbol is IMethodSymbol owningMethod
                    && IsImplementationOfBuildRenderTree(owningMethod, availableTypes))
                {
                    foreach (var childBlock in blockContext.OperationBlocks)
                    {
                        // Should be one but could be more than one if there are multiple partial class definitions.
                        AnalyzeOperationsTree(childBlock, availableTypes, analyzerState);
                    }
                    analyzerState.ReportDiagnostics(blockContext);
                }
            });
        });
    }
 
    private static bool IsImplementationOfBuildRenderTree(IMethodSymbol methodSymbol, Dictionary<string, INamedTypeSymbol?> availableTypes)
    {
        if (methodSymbol.Name != "BuildRenderTree" || !methodSymbol.IsOverride)
        {
            return false;
        }
        var containingType = methodSymbol.ContainingType;
        while (containingType is not null)
        {
            if (SymbolEqualityComparer.Default.Equals(containingType, availableTypes[ComponentsApi.ComponentBase.MetadataName]))
            {
                return true;
            }
            containingType = containingType.BaseType;
        }
        return false;
    }
 
    private static void AnalyzeOperationsTree(IOperation operation, Dictionary<string, INamedTypeSymbol?> availableTypes, ForLoopAnalyzerState analyzerState)
    {
        if (operation is IForLoopOperation forLoopOperation)
        {
            AnalyzeForLoopVariables(forLoopOperation, analyzerState);
            AnalyzeOperationsTree(forLoopOperation.Body, availableTypes, analyzerState);
        }
        else if (operation is IIncrementOrDecrementOperation incrementOrDecrement)
        {
            AnalyzeIncrementOrDecrement(incrementOrDecrement, analyzerState);
        }
        else if (operation is IAssignmentOperation assignment)
        {
            AnalyzeAssignment(assignment, analyzerState);
        }
        else if (operation is IInvocationOperation invocation)
        {
            AnalyzeInvocation(invocation, availableTypes, analyzerState);
        }
        else
        {
            // Expression statements, blocks, switch, cases etc. that have children.
            // During source-build, we use newer version of Microsoft.CodeAnalysis which obsoletes Children and provide an alternative.
            // However, for normal builds, we use MS.CA 3.3.1 which doesn't obsolete Children and doesn't have an alternative.
            // We suppress the warning for source-builds for now.
#pragma warning disable CS0618
            foreach (var childOperation in operation.Children)
#pragma warning restore CS0618
            {
                AnalyzeOperationsTree(childOperation, availableTypes, analyzerState);
            }
        }
    }
 
    private static void AnalyzeForLoopVariables(IForLoopOperation forLoopOperation, ForLoopAnalyzerState analyzerState)
    {
        if (forLoopOperation is null)
        {
            return;
        }
 
        // Get all incremented variables.
        foreach (var bottomOperation in forLoopOperation.AtLoopBottom)
        {
            if (bottomOperation is IExpressionStatementOperation expression)
            {
                if (expression.Operation is IIncrementOrDecrementOperation operation
                    && operation.Target is ILocalReferenceOperation target)
                {
                    analyzerState.AddIterator(target.Local);
                }
                else if (expression.Operation is IAssignmentOperation assignment
                    && assignment.Target is ILocalReferenceOperation assignmentTarget)
                {
                    analyzerState.AddIterator(assignmentTarget.Local);
                }
            }
        }
 
        // The rest add as potentials.
        foreach (var localVar in forLoopOperation.Locals)
        {
            analyzerState.AddPotentialIterator(localVar);
        }
    }
 
    private static void AnalyzeInvocation(IInvocationOperation invocation, Dictionary<string, INamedTypeSymbol?> availableTypes, ForLoopAnalyzerState analyzerState)
    {
        if (analyzerState.AllForVariables.Count == 0)
        {
            return;
        }
 
        if (invocation.Instance?.Type is not null
            && SymbolEqualityComparer.Default.Equals(invocation.Instance.Type, availableTypes[ComponentsApi.RenderTreeBuilder.MetadataName])
            && invocation.Arguments.Length >= 3)
        {
            var targetMethod = invocation.TargetMethod;
            if (targetMethod.Name != "AddAttribute" && targetMethod.Name != "AddComponentParameter")
            {
                return;
            }
 
            // Get the operation that when containing variable reference could cause an issue. Usually an anonymous function or @bind
            IOperation? suspectOperation = null;
            var valueArgument = invocation.Arguments[2].Value;
            if (valueArgument is IInvocationOperation valueInvocation)
            {
                if (SymbolEqualityComparer.Default.Equals(valueInvocation.TargetMethod.ContainingType, availableTypes[ComponentsApi.BindConverter.MetadataName]))
                {
                    // Use the 'value' attribute setter when using @bind, for the location to be more accurate when reporting.
                    suspectOperation = valueInvocation;
                }
                else if (SymbolEqualityComparer.Default.Equals(valueInvocation.TargetMethod.ContainingType, availableTypes[ComponentsApi.EventCallbackFactory.MetadataName])
                    && valueInvocation.TargetMethod.Name != "CreateBinder"
                    && valueInvocation.Arguments.Length >= 2
                    && valueInvocation.Arguments[1].Value is IDelegateCreationOperation delegateCreation
                    && delegateCreation.Target is IAnonymousFunctionOperation)
                {
                    // The anonymous function of a basic event callback.
                    suspectOperation = delegateCreation.Target;
                }
            }
            else if (valueArgument is IDelegateCreationOperation delegateCreation
                && delegateCreation.Target is IAnonymousFunctionOperation)
            {
                suspectOperation = delegateCreation.Target;
            }
            else if (valueArgument is IConversionOperation conversionOperation
                && conversionOperation.Operand.Type is not null
                && conversionOperation.Operand.Type.ContainingNamespace.ToString().StartsWith(ComponentsApi.AssemblyName, StringComparison.Ordinal))
            {
                // If the value is a conversion operation, search if a delegate is created like RenderFragment or an EventCallback.
                // Multiple delegates at once shouldn't be possible.
                var delegateResult = FindFirstDelegateChild(conversionOperation.Operand);
                if (delegateResult is not null && delegateResult.Target is IAnonymousFunctionOperation)
                {
                    suspectOperation = delegateResult.Target;
                }
            }
 
            if (suspectOperation is not null)
            {
                var usedVariables = suspectOperation.Descendants().OfType<ILocalReferenceOperation>();
                analyzerState.AddRelatedOccurrences(usedVariables);
            }
        }
    }
 
    private static void AnalyzeIncrementOrDecrement(IIncrementOrDecrementOperation incrementOrDecrement, ForLoopAnalyzerState analyzerState)
    {
        if (incrementOrDecrement.Target is ILocalReferenceOperation localReference
            && analyzerState.IsPotentialIterator(localReference.Local))
        {
            analyzerState.OnPotentialIteratorChanged(localReference.Local);
        }
    }
 
    private static void AnalyzeAssignment(IAssignmentOperation assignment, ForLoopAnalyzerState analyzerState)
    {
        if (assignment.Target is ILocalReferenceOperation localReference
            && analyzerState.IsPotentialIterator(localReference.Local))
        {
            analyzerState.OnPotentialIteratorChanged(localReference.Local);
        }
    }
 
    /// <summary>
    /// Probe an operation for delegate creation. If found, return the first occurrence. Otherwise search only inside Conversion or Invocation operations.
    /// Should be more efficient than calling Descendants() and filtering for IDelegateCreationOperation, since we don't need to search inside all operations.
    /// </summary>
    private static IDelegateCreationOperation? FindFirstDelegateChild(IOperation currentOperation)
    {
        if (currentOperation is IDelegateCreationOperation delegateCreation)
        {
            return delegateCreation;
        }
 
        if (currentOperation is IArgumentOperation 
            || currentOperation is IConversionOperation 
            || currentOperation is IInvocationOperation)
        {
            // During source-build, we use newer version of Microsoft.CodeAnalysis which obsoletes Children and provide an alternative.
            // However, for normal builds, we use MS.CA 3.3.1 which doesn't obsolete Children and doesn't have an alternative.
            // We suppress the warning for source-builds for now.
#pragma warning disable CS0618
            foreach (var child in currentOperation.Children)
#pragma warning restore CS0618
            {
                var result = FindFirstDelegateChild(child);
                if (result is not null)
                {
                    return result;
                }
            }
        }
        return null;
    }
 
    /// <summary>
    /// State to keep track of variables that are being incremented or potentially incremented and their occurrences in the current for loop.
    /// </summary>
    private sealed class ForLoopAnalyzerState
    {
        // Variables that are being incremented on in the current context. 
        public List<ILocalSymbol> Iterators { get; } = new();
        // Potential variable that are not yet being incremented on which would not cause any issues so far.
        public List<ILocalSymbol> PotentialIterators { get; private set; } = new();
 
        // All variable that are being used in the current context. 
        public List<ILocalSymbol> AllForVariables { get; } = new();
 
        // Occurrences of potential variables that could be later on incremented. Not inherited from parent state.
        public List<ILocalReferenceOperation> IteratorOccurrences { get; } = new();
 
        public bool IsIterator(ILocalSymbol target)
        {
            return Iterators.Any(existing => SymbolEqualityComparer.Default.Equals(existing, target));
        }
 
        public bool IsPotentialIterator(ILocalSymbol? target)
        {
            return target is not null && PotentialIterators.Any(existing => SymbolEqualityComparer.Default.Equals(existing, target));
        }
 
        public void AddIterator(ILocalSymbol iterator)
        {
            if (!IsIterator(iterator))
            {
                Iterators.Add(iterator);
                AllForVariables.Add(iterator);
            }
        }
 
        public void AddPotentialIterator(ILocalSymbol potentialIterator)
        {
            if (!IsIterator(potentialIterator) && !IsPotentialIterator(potentialIterator))
            {
                PotentialIterators.Add(potentialIterator);
                AllForVariables.Add(potentialIterator);
            }
        }
 
        /// <summary>
        /// When potential variable is incremented, move it to incremented variables and report diagnostics for all previous occurrences of it in the current context.
        /// </summary>
        public void OnPotentialIteratorChanged(ILocalSymbol potentialIterator)
        {
            Iterators.Add(potentialIterator);
            PotentialIterators = PotentialIterators.Where(name => !SymbolEqualityComparer.Default.Equals(name, potentialIterator)).ToList();
        }
 
        public void AddRelatedOccurrences(IEnumerable<ILocalReferenceOperation> variableReferences)
        {
            foreach (var variableReference in variableReferences)
            {
                if (AllForVariables.Any(forVariable => SymbolEqualityComparer.Default.Equals(forVariable, variableReference.Local)))
                {
                    IteratorOccurrences.Add(variableReference);
                }
            }
        }
 
        public void ReportDiagnostics(OperationBlockAnalysisContext context)
        {
            var referencesToReport = IteratorOccurrences.Where(reference => IsIterator(reference.Local));
            foreach (var reference in referencesToReport)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    DiagnosticDescriptors.ForLoopIteratorVariableUsedInClosure,
                    reference.Syntax.GetLocation(),
                    reference.Local.Name));
            }
        }
    }
}