Skip to content

Range variables in LINQ query expressions should be allowed to shadow locals and parameters #83134

@jnm2

Description

@jnm2

Version Used: 18.4.0

Summary

The Roslyn compiler incorrectly rejects LINQ query expressions where a range variable has the same name as a local variable or parameter in the enclosing scope. The C# specification explicitly allows this: query expressions are syntactically expanded into lambda expressions, and lambda parameters are permitted to shadow names in enclosing scopes via the "hiding through nesting" mechanism, and the scope of range variables is determined by the syntactic expansion.

Simple demonstration

void M(int x)
{
    // CS1931: "The range variable 'x' conflicts with a previous declaration of 'x'"
    var q = from x in new[] { 1, 2, 3 }
            select x;
}

The compiler produces CS1931, but according to the spec this should be legal. The specification defines that the query expression is syntactically expanded to:

void M(int x)
{
    var q = (new[] { 1, 2, 3 }).Select(x => x);
}

And a lambda parameter x shadowing the method parameter x is explicitly allowed by the spec — in fact, the directly equivalent hand-written lambda compiles without error:

void M(int x)
{
    // This compiles fine!
    var q = (new[] { 1, 2, 3 }).Select(x => x);
}

Why the spec allows this

The spec defines two distinct kinds of local variable declaration space nesting, with different rules:

Function-level nesting (shadowing allowed)

§7.3, paragraph starting "Each method declaration..." (line 84):

Each method declaration, property declaration, [...] anonymous function, and local function creates a new declaration space called a local variable declaration space. [...] When a local variable declaration space and a nested local variable declaration space contain elements with the same name, within the scope of the nested local name, the outer local name is hidden (§7.7.1) by the nested local name.

This is the rule that permits lambda parameters to shadow outer names. When an anonymous function (lambda) introduces a parameter with the same name as something in an enclosing scope, it hides it — no error.

Block-level nesting (shadowing prohibited)

§7.3, next paragraph (line 85):

Additional local variable declaration spaces may occur within member declarations, anonymous functions and local functions. [...] Local variable declaration spaces may be nested, but it is an error for a local variable declaration space and a nested local variable declaration space to contain elements with the same name. Thus, within a nested declaration space it is not possible to declare a local variable, local function or constant with the same name as a parameter, type parameter, local variable, local function or constant in an enclosing declaration space.

This is the rule that prohibits re-declaring a name within a nested block inside the same function. But this rule governs block-level nesting, not function-level (lambda) nesting.

Query expressions produce lambdas

§7.3, bullet for query expressions (line 92):

The syntactic translation of a query_expression (§12.22.3) may introduce one or more lambda expressions. As anonymous functions, each of these creates a local variable declaration space as described above.

This tells us range variables, after translation, become lambda parameters. They create function-level declaration spaces, meaning the first rule (hiding/shadowing allowed) applies — not the second rule (error).

Scope is determined by expansion

§7.7.1 (line 630):

The scope of a variable declared as part of a foreach_statement, using_statement, lock_statement or query_expression is determined by the expansion of the given construct.

Hiding through nesting explicitly includes lambdas

§7.7.2.2 (line 723):

Name hiding through nesting can occur as a result of nesting namespaces or types within namespaces, as a result of nesting types within classes or structs, as a result of a local function or a lambda, and as a result of parameter, local variable, and local constant declarations.

The example in that section even demonstrates a lambda parameter i hiding a local float i:

class A
{
    int i = 0;
    void F()
    {
        int i = 1;

        void M1()
        {
            float i = 1.0f;
            Func<double, double> doubler = (double i) => i * 2.0;
        }
    }
}

All ways range variables are introduced

The spec defines range variables through five syntactic constructs. Each one should be allowed to shadow enclosing locals/parameters, because the syntactic expansion of each does not create a conflict with local identifiers outside the LINQ expression.


1. from clause

Grammar (§12.22.1):

from_clause
    : 'from' type? identifier 'in' expression
    ;

The identifier in a from clause introduces a range variable.

Example — should be legal:

void M(int x)
{
    var q = from x in new[] { 1, 2, 3 }
            select x * 2;
}

Expansion (§12.22.3.6):

void M(int x)
{
    var q = (new[] { 1, 2, 3 }).Select(x => x * 2);
    //                                 ^ lambda parameter shadows method parameter — allowed
}

Current behavior: CS1931 error.


2. Second from clause (SelectMany)

Example — should be legal:

void M(int x, int y)
{
    var q = from x in new[] { 1, 2, 3 }
            from y in new[] { 4, 5, 6 }
            select x + y;
}

Expansion (§12.22.3.5):

void M(int x, int y)
{
    var q = (new[] { 1, 2, 3 }).SelectMany(
        x => new[] { 4, 5, 6 },
        (x, y) => x + y);
    // Both lambda parameters shadow method parameters — allowed
}

Current behavior: CS1931 error on both x and y.


3. let clause

Grammar (§12.22.1):

let_clause
    : 'let' identifier '=' expression
    ;

Example — should be legal:

void M(int t)
{
    var q = from o in orders
            let t = o.Total * 1.1
            select new { o.Id, Tax = t };
}

Expansion (§12.22.3.5):

A let clause is translated into a Select producing an anonymous type, followed by transparent-identifier propagation:

from o in orders
let t = o.Total * 1.1
select new { o.Id, Tax = t }

becomes:

from * in (orders).Select(o => new { o, t = o.Total * 1.1 })
select new { o.Id, Tax = t }

Final translation (with transparent identifiers erased):

(orders)
    .Select(o => new { o, t = o.Total * 1.1 })
    .Select(x => new { x.o.Id, Tax = x.t });

The range variable t introduced by let becomes part of a lambda parameter's anonymous type member. The o in the initial Select is a lambda parameter that could shadow an outer o. In either case, these are lambda parameters in function-level declaration spaces — shadowing is allowed.

Current behavior: CS1931 error on t.


4. join clause

Grammar (§12.22.1):

join_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression
    ;

Example — should be legal:

void M(int o)
{
    var q = from c in customers
            join o in orders on c.Id equals o.CustomerId
            select new { c.Name, o.Total };
}

Expansion (§12.22.3.5):

void M(int o)
{
    var q = (customers).Join(
        orders,
        c => c.Id,
        o => o.CustomerId,         // lambda parameter 'o' shadows method parameter
        (c, o) => new { c.Name, o.Total });
}

Current behavior: CS1931 error on o.


5. join-into clause (GroupJoin)

Grammar (§12.22.1):

join_into_clause
    : 'join' type? identifier 'in' expression 'on' expression
      'equals' expression 'into' identifier
    ;

Both the join's range variable and the into identifier introduce range variables.

Example — should be legal:

void M(int co)
{
    var q = from c in customers
            join o in orders on c.Id equals o.CustomerId into co
            select new { c.Name, OrderCount = co.Count() };
}

Expansion (§12.22.3.5):

void M(int co)
{
    var q = (customers).GroupJoin(
        orders,
        c => c.Id,
        o => o.CustomerId,
        (c, co) => new { c.Name, OrderCount = co.Count() });
    //     ^ lambda parameter 'co' shadows method parameter — allowed
}

Current behavior: CS1931 error on co.


6. Query continuation (into)

Grammar (§12.22.1):

query_continuation
    : 'into' identifier query_body
    ;

Example — should be legal:

void M(int g)
{
    var q = from c in customers
            group c by c.Country into g
            select new { Country = g.Key, Count = g.Count() };
}

Expansion (§12.22.3.2):

First, the continuation is rewritten:

from g in (from c in customers group c by c.Country)
select new { Country = g.Key, Count = g.Count() }

Then final translation:

void M(int g)
{
    var q = customers
        .GroupBy(c => c.Country)
        .Select(g => new { Country = g.Key, Count = g.Count() });
    //         ^ lambda parameter 'g' shadows method parameter — allowed
}

Current behavior: CS1931 error on g.


Summary of affected error codes

Error Message Should be
CS1931 "The range variable '{name}' conflicts with a previous declaration of '{name}'" No error (shadowing via lambda parameter)

The logic chain

  1. Query expressions are translated into method calls with lambda arguments (§12.22.3.1)
  2. Range variables become lambda parameters in the translated form (§12.22.3 — all translation rules)
  3. Lambda expressions (anonymous functions) create function-level local variable declaration spaces (§7.3, line 84 + line 92)
  4. Function-level declaration spaces allow same-named elements in nested spaces — the inner name hides the outer (§7.3, line 84)
  5. This hiding is explicitly described as applying to lambdas (§7.7.2.2, line 723)
  6. The scope of range variables is determined by the expansion (§7.7.1, line 630)
  7. There is no query-specific rule in the spec that restricts range variable names from matching enclosing locals or parameters — the only query-specific restriction is on assignment to range variables and using them as ref/out arguments (§12.22.3.1, line 6025)
  8. Therefore, CS1931 is incorrectly applied

Consistency with direct lambda usage

The inconsistency is easy to demonstrate. These two should behave identically, but only the second compiles:

void M(IEnumerable<int> items, int x)
{
    // ERROR: CS1931
    var a = from x in items select x;

    // OK: compiles fine
    var b = items.Select(x => x);
}

Since query expressions are defined purely as syntactic sugar over method calls with lambdas, they should have identical scoping behavior.

Spec references (all links to draft-v8 branch)

Metadata

Metadata

Assignees

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions