File: src\Analyzers\Core\Analyzers\UseCompoundAssignment\UseCompoundAssignmentUtilities.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.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis.LanguageService;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.UseCompoundAssignment;
 
internal static class UseCompoundAssignmentUtilities
{
    internal const string Increment = nameof(Increment);
    internal const string Decrement = nameof(Decrement);
 
    public static void GenerateMaps<TSyntaxKind>(
        ImmutableArray<(TSyntaxKind exprKind, TSyntaxKind assignmentKind, TSyntaxKind tokenKind)> kinds,
        out ImmutableDictionary<TSyntaxKind, TSyntaxKind> binaryToAssignmentMap,
        out ImmutableDictionary<TSyntaxKind, TSyntaxKind> assignmentToTokenMap) where TSyntaxKind : struct
    {
        var binaryToAssignmentBuilder = ImmutableDictionary.CreateBuilder<TSyntaxKind, TSyntaxKind>();
        var assignmentToTokenBuilder = ImmutableDictionary.CreateBuilder<TSyntaxKind, TSyntaxKind>();
 
        foreach (var (exprKind, assignmentKind, tokenKind) in kinds)
        {
            binaryToAssignmentBuilder[exprKind] = assignmentKind;
            assignmentToTokenBuilder[assignmentKind] = tokenKind;
        }
 
        binaryToAssignmentMap = binaryToAssignmentBuilder.ToImmutable();
        assignmentToTokenMap = assignmentToTokenBuilder.ToImmutable();
 
        Debug.Assert(binaryToAssignmentMap.Count == assignmentToTokenMap.Count);
        Debug.Assert(binaryToAssignmentMap.Values.All(assignmentToTokenMap.ContainsKey));
    }
 
    public static bool IsSideEffectFree(
        ISyntaxFacts syntaxFacts, SyntaxNode expr, SemanticModel semanticModel, CancellationToken cancellationToken)
    {
        return IsSideEffectFreeRecurse(syntaxFacts, expr, semanticModel, isTopLevel: true, cancellationToken);
    }
 
    private static bool IsSideEffectFreeRecurse(
        ISyntaxFacts syntaxFacts, SyntaxNode expr, SemanticModel semanticModel,
        bool isTopLevel, CancellationToken cancellationToken)
    {
        if (expr == null)
            return false;
 
        // it basically has to be of the form "a.b.c", where all components are locals,
        // parameters or fields.  Basically, nothing that can cause arbitrary user code
        // execution when being evaluated by the compiler.
 
        if (syntaxFacts.IsThisExpression(expr) ||
            syntaxFacts.IsBaseExpression(expr) ||
            syntaxFacts.IsFieldExpression(expr))
        {
            // Referencing this/base like  this.a.b.c causes no side effects itself.
            return true;
        }
 
        if (syntaxFacts.IsIdentifierName(expr))
        {
            return IsSideEffectFreeSymbol(expr, semanticModel, isTopLevel, cancellationToken);
        }
 
        if (syntaxFacts.IsParenthesizedExpression(expr))
        {
            syntaxFacts.GetPartsOfParenthesizedExpression(expr,
                out _, out var expression, out _);
 
            return IsSideEffectFreeRecurse(syntaxFacts, expression, semanticModel, isTopLevel, cancellationToken);
        }
 
        if (syntaxFacts.IsSimpleMemberAccessExpression(expr))
        {
            syntaxFacts.GetPartsOfMemberAccessExpression(expr,
                out var subExpr, out _);
            return IsSideEffectFreeRecurse(syntaxFacts, subExpr, semanticModel, isTopLevel: false, cancellationToken) &&
                   IsSideEffectFreeSymbol(expr, semanticModel, isTopLevel, cancellationToken);
        }
 
        if (syntaxFacts.IsConditionalAccessExpression(expr))
        {
            syntaxFacts.GetPartsOfConditionalAccessExpression(expr,
                out var expression, out var whenNotNull);
            return IsSideEffectFreeRecurse(syntaxFacts, expression, semanticModel, isTopLevel: false, cancellationToken) &&
                   IsSideEffectFreeRecurse(syntaxFacts, whenNotNull, semanticModel, isTopLevel: false, cancellationToken);
        }
 
        // Something we don't explicitly handle.  Assume this may have side effects.
        return false;
    }
 
    private static bool IsSideEffectFreeSymbol(
        SyntaxNode expr, SemanticModel semanticModel, bool isTopLevel, CancellationToken cancellationToken)
    {
        var symbolInfo = semanticModel.GetSymbolInfo(expr, cancellationToken);
        if (symbolInfo.CandidateSymbols.Length > 0 ||
            symbolInfo.Symbol == null)
        {
            // couldn't bind successfully, assume that this might have side-effects.
            return false;
        }
 
        var symbol = symbolInfo.Symbol;
        switch (symbol.Kind)
        {
            case SymbolKind.Namespace:
            case SymbolKind.NamedType:
            case SymbolKind.Field:
            case SymbolKind.Parameter:
            case SymbolKind.Local:
                return true;
        }
 
        if (isTopLevel && symbol is IPropertySymbol { RefKind: RefKind.None })
        {
            // If we have `this.Prop = this.Prop * 2`, then that's just a single read/write of
            // the prop and we can safely make that `this.Prop *= 2` (since it will still be a
            // single read/write).  However, if we had `this.prop.x = this.prop.x * 2`, then
            // that's multiple reads of `this.prop`, and it's not safe to convert that to
            // `this.prop.x *= 2` in the case where calling 'prop' may have side effects.
            //
            // Note, this doesn't apply if the property is a ref-property.  In that case, we'd
            // go from a read and a write to to just a read (and a write to it's returned ref
            // value).
            return true;
        }
 
        return false;
    }
}