Range variables in LINQ query expressions should be allowed to shadow locals and parameters
- Dominant language
- C#
- Stars
- 20.7k
- Forks
- 4.3k
- PR merge metrics
- PR metrics pending
Description
**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
```csharp
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:
```csharp
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:
```csharp
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..."](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#73-declarations) (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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#73-declarations) (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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#73-declarations) (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**](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#771-general) (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**](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#7722-hiding-through-nesting) (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`:
```csharp
class A
{
int i = 0;
void F()
{
int i = 1;
void M1()
{
float i = 1.0f;
Func 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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12221-general)):
```
from_clause
: 'from' type? identifier 'in' expression
;
```
The identifier in a `from` clause introduces a range variable.
**Example — should be legal:**
```csharp
void M(int x)
{
var q = from x in new[] { 1, 2, 3 }
select x * 2;
}
```
**Expansion** ([§12.22.3.6](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122236-select-clauses)):
```csharp
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:**
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122235-from-let-where-join-and-orderby-clauses)):
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12221-general)):
```
let_clause
: 'let' identifier '=' expression
;
```
**Example — should be legal:**
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122235-from-let-where-join-and-orderby-clauses)):
A `let` clause is translated into a `Select` producing an anonymous type, followed by transparent-identifier propagation:
```csharp
from o in orders
let t = o.Total * 1.1
select new { o.Id, Tax = t }
```
becomes:
```csharp
from * in (orders).Select(o => new { o, t = o.Total * 1.1 })
select new { o.Id, Tax = t }
```
Final translation (with transparent identifiers erased):
```csharp
(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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12221-general)):
```
join_clause
: 'join' type? identifier 'in' expression 'on' expression
'equals' expression
;
```
**Example — should be legal:**
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122235-from-let-where-join-and-orderby-clauses)):
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12221-general)):
```
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:**
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122235-from-let-where-join-and-orderby-clauses)):
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12221-general)):
```
query_continuation
: 'into' identifier query_body
;
```
**Example — should be legal:**
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122232-query-expressions-with-continuations)):
First, the continuation is rewritten:
```csharp
from g in (from c in customers group c by c.Country)
select new { Country = g.Key, Count = g.Count() }
```
Then final translation:
```csharp
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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122231-general))
2. Range variables become lambda parameters in the translated form ([§12.22.3](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12223-query-expression-translation) — all translation rules)
3. Lambda expressions (anonymous functions) create function-level local variable declaration spaces ([§7.3](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#73-declarations), line 84 + line 92)
4. Function-level declaration spaces **allow** same-named elements in nested spaces — the inner name **hides** the outer ([§7.3](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#73-declarations), line 84)
5. This hiding is explicitly described as applying to lambdas ([§7.7.2.2](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#7722-hiding-through-nesting), line 723)
6. The scope of range variables is determined by the expansion ([§7.7.1](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#771-general), 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](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122231-general), 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:
```csharp
void M(IEnumerable 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)
- [§7.3 Declarations](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#73-declarations) — declaration space rules
- [§7.7.1 Scopes — General](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#771-general) — scope determined by expansion
- [§7.7.2.2 Hiding through nesting](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/basic-concepts.md#7722-hiding-through-nesting) — lambdas can hide outer names
- [§12.22.1 General (Query expressions)](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12221-general) — grammar and range variable introduction
- [§12.22.3 Query expression translation](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12223-query-expression-translation) — all translation rules
- [§12.22.3.8 Transparent identifiers](https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#122238-transparent-identifiers) — transparent identifier mechanics
Contributor guide
Assessment
This issue has not been assessed yet.