Skip to content

Avoid walking the whole operation tree in IsInExpressionContext - #1409

Merged
meziantou merged 1 commit into
mainfrom
feature/isinexpressioncontext-perf-b821de
Sep 6, 2026
Merged

meziantou merged 1 commit into
mainfrom
feature/isinexpressioncontext-perf-b821de

Conversation

@meziantou

Copy link
Copy Markdown
Owner

What

OperationUtilities.IsInExpressionContext now walks operation.Parent directly instead of the Ancestors() iterator, and stops at the first enclosing IBlockOperation that is not the body of a lambda.

for (var op = operation.Parent; op is not null; op = op.Parent)
{
    switch (op)
    {
        case IArgumentOperation { Parameter: { } parameter } when parameter.Type.InheritsFrom(_expressionSymbol):
            return true;

        case IConversionOperation { Type: { } type } when type.InheritsFrom(_expressionSymbol):
            return true;

        // An expression tree can only be entered by converting a lambda, so the search can stop at the first
        // enclosing body that is not the body of a lambda (method body, local function body, nested block, ...)
        case IBlockOperation when op.Parent is not IAnonymousFunctionOperation:
            return false;
    }
}

Why

The previous implementation enumerated operation.Ancestors() all the way to the root of the operation tree, allocating a yield return state machine on every call and calling InheritsFrom(System.Linq.Expressions.Expression) on every IArgumentOperation and IConversionOperation it passed. Ten analyzers call it — UseStringComparer, UseStringComparison, UseStringEquals, UsePatternMatchingForEqualityComparisons, DoNotUseEqualityOperatorsForSpanOfChar, AwaitAwaitableMethodInSyncMethod, NamedParameter, UseAwaitInsteadOfReturningTask, UsePatternMatchingInsteadOfHasValue — several of them on the densest operation kinds (Invocation, Binary), and the common answer is false, which meant walking the full depth every time.

Note for reviewers

The lambda-body exception is what keeps the early exit correct. IAnonymousFunctionOperation.Body is always an IBlockOperation, even for the expression-bodied lambdas that expression trees require, so the walk must pass through that one block to reach the Expression<>-typed conversion or argument above it. Every other block — method body, local function body, nested statement block — cannot sit under an expression tree, so it terminates the walk. No separate IMethodBodyOperation case is needed: the body's block is reached first and stops there.

Tests

  • Added DisabledInExpression_NestedInBlocks to UsePatternMatchingForEqualityComparisonsAnalyzerTests, covering the IArgumentOperation path (IQueryable.Where(item => item == 0)) nested inside a statement block. The existing DisabledInExpression only covered the cast/conversion path.
  • Confirmed the guard has teeth: replacing the case with a naive case IBlockOperation: makes both DisabledInExpression tests fail, because MA0140 is then reported inside the expression tree.
  • Full suite across all five Roslyn versions: 19263 passed, 0 failed.
  • dotnet run --project src/DocumentationGenerator exits 0 with no markdown changes.

IsInExpressionContext enumerated operation.Ancestors() up to the root of
the operation tree, allocating an iterator state machine per call and
calling InheritsFrom on every argument and conversion along the way. Ten
analyzers call it, several of them on the densest operation kinds
(Invocation, Binary), and the common answer is false.

Walk operation.Parent directly instead of the iterator, and stop at the
first enclosing IBlockOperation that is not the body of a lambda. An
expression tree can only be entered by converting a lambda, so a method
body, a local function body or a nested statement block cannot be inside
one. The lambda body itself must still be traversed, as
IAnonymousFunctionOperation.Body is an IBlockOperation even for the
expression-bodied lambdas that expression trees require.
@meziantou
meziantou enabled auto-merge (squash) September 6, 2026 04:21
@meziantou
meziantou merged commit d30c659 into main Sep 6, 2026
13 checks passed
@meziantou
meziantou deleted the feature/isinexpressioncontext-perf-b821de branch September 6, 2026 04:22
This was referenced Sep 6, 2026
This was referenced Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant