Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
9c2c794
Initial plan
Copilot Nov 16, 2025
bc76ea7
Fix CS1003 error message to suggest '=' when literal follows identifier
Copilot Nov 16, 2025
eb24d70
Add WorkItem attribute to test methods for issue #44292
Copilot Nov 16, 2025
6018aa5
Extend parser fix to handle all expressions and use raw string literals
Copilot Nov 16, 2025
2ed35f5
Update src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
CyrusNajmabadi Nov 16, 2025
05ca746
Remove IsTrueIdentifier check and add OpenBraceToken handling
Copilot Nov 16, 2025
4806bf7
Add documentation for comprehensive manual test verification
Copilot Nov 16, 2025
ba3c97e
Fix comma/semicolon/equals check to apply only for identifiers
Copilot Nov 16, 2025
1affc1b
Remove incorrect comment and add proper manual verification documenta…
Copilot Nov 16, 2025
804703f
Simplify
CyrusNajmabadi Nov 16, 2025
9444e48
Merge branch 'copilot/fix-variable-declaration-issue' of https://gith…
CyrusNajmabadi Nov 16, 2025
138ba72
Fix check
CyrusNajmabadi Nov 16, 2025
a86561d
Add actual tests for unary, binary, and object creation expressions
Copilot Nov 16, 2025
67b2d89
Fix check
CyrusNajmabadi Nov 16, 2025
c823254
Merge branch 'copilot/fix-variable-declaration-issue' of https://gith…
CyrusNajmabadi Nov 16, 2025
f6f16d3
Add testss
CyrusNajmabadi Nov 16, 2025
410ba8a
Fixup
CyrusNajmabadi Nov 16, 2025
6ecd0f5
Simplify impl
CyrusNajmabadi Nov 16, 2025
bb5b40b
Update tests
CyrusNajmabadi Nov 16, 2025
9ec48ac
Update tests
CyrusNajmabadi Nov 16, 2025
c6a0d04
Fix test
CyrusNajmabadi Nov 17, 2025
5abb35e
Update test
CyrusNajmabadi Nov 17, 2025
7dde080
REvert
CyrusNajmabadi Nov 17, 2025
4a2161f
Update src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
CyrusNajmabadi Nov 17, 2025
4c3122b
Add assert
CyrusNajmabadi Nov 17, 2025
6fcd389
Merge branch 'copilot/fix-variable-declaration-issue' of https://gith…
CyrusNajmabadi Nov 17, 2025
cd1b2ff
Fix test
CyrusNajmabadi Nov 17, 2025
a89f601
Merge remote-tracking branch 'upstream/main' into copilot/fix-variabl…
CyrusNajmabadi Nov 26, 2025
99a6f66
Add assert
CyrusNajmabadi Nov 26, 2025
1d31995
Move to end
CyrusNajmabadi Nov 26, 2025
f346c3b
Add tests
CyrusNajmabadi Nov 26, 2025
6c49f4d
Add tests
CyrusNajmabadi Nov 26, 2025
b87edba
Add tests
CyrusNajmabadi Nov 26, 2025
f86f217
Add tests
CyrusNajmabadi Nov 26, 2025
e4bc551
Add tests
CyrusNajmabadi Nov 26, 2025
7cdc933
Add tests
CyrusNajmabadi Nov 26, 2025
0ee7743
Improve parsing logic
CyrusNajmabadi Nov 26, 2025
1d0772a
in progress
CyrusNajmabadi Nov 26, 2025
f2168e5
Add comments and tests
CyrusNajmabadi Nov 26, 2025
2eedc63
Merge branch 'copilot/fix-variable-declaration-issue' into betterSwitch
CyrusNajmabadi Nov 26, 2025
ca928a9
Add tests
CyrusNajmabadi Nov 26, 2025
ef31f06
Merge branch 'copilot/fix-variable-declaration-issue' into betterSwitch
CyrusNajmabadi Nov 26, 2025
fdb75e4
Add asserts
CyrusNajmabadi Nov 26, 2025
82b95b8
Add asserts
CyrusNajmabadi Nov 26, 2025
1d136b8
Fix spelling
CyrusNajmabadi Nov 26, 2025
f94459c
revert
CyrusNajmabadi Nov 26, 2025
e0fa39d
Merge branch 'copilot/fix-variable-declaration-issue' into betterSwitch
CyrusNajmabadi Nov 26, 2025
1124b8b
Simplify
CyrusNajmabadi Nov 26, 2025
250fe07
Restore
CyrusNajmabadi Nov 26, 2025
ce54c67
Restore
CyrusNajmabadi Nov 26, 2025
de2774b
Add comment
CyrusNajmabadi Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 172 additions & 94 deletions src/Compilers/CSharp/Portable/Parser/LanguageParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5506,9 +5506,6 @@ private VariableDeclaratorSyntax ParseVariableDeclarator(
// specifically treats it as a variable name, even if it could be interpreted as a
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

view with whitespace off so the diff is reasonable.

// keyword.
var name = this.ParseIdentifierToken();
BracketedArgumentListSyntax argumentList = null;
EqualsValueClauseSyntax initializer = null;
TerminatorState saveTerm = _termState;
bool isFixed = (flags & VariableFlags.Fixed) != 0;
bool isConst = (flags & VariableFlags.Const) != 0;
bool isLocalOrField = (flags & VariableFlags.LocalOrField) != 0;
Expand All @@ -5524,125 +5521,206 @@ private VariableDeclaratorSyntax ParseVariableDeclarator(
name = this.AddError(name, ErrorCode.ERR_MultiTypeInDeclaration);
}

switch (this.CurrentToken.Kind)
return this.CurrentToken.Kind switch
{
case SyntaxKind.EqualsToken:
if (isFixed)
{
goto default;
}
SyntaxKind.EqualsToken when !isFixed => parseNonFixedVariableDeclaratorWithEqualsToken(name, argumentList: null, out localFunction),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prevents a confusing jump inside parseNonFixedVariableDeclaratorWithEqualsToken to parseVariableDeclaratorDefault. This simplifies things as we don't have to wonder how an argumentList might flow through all codepaths to these helpers.

SyntaxKind.LessThanToken => parseVariableDeclaratorWithLessThanToken(name, out localFunction),
SyntaxKind.OpenParenToken => parseVariableDeclaratorWithOpenParenToken(name, out localFunction),
SyntaxKind.OpenBracketToken => parseVariableDeclaratorWithOpenBracketToken(name, out localFunction),
_ => parseVariableDeclaratorDefault(name, out localFunction),
};

var equals = this.EatToken();
VariableDeclaratorSyntax parseNonFixedVariableDeclaratorWithEqualsToken(
SyntaxToken name, BracketedArgumentListSyntax argumentList, out LocalFunctionStatementSyntax localFunction)
{
Debug.Assert(this.CurrentToken.Kind == SyntaxKind.EqualsToken);
Debug.Assert(!isFixed, "Should only be called in the non fixed-statement/fixed-size-buffer case");

// check for lambda expression with explicit ref return type: `ref int () => { ... }`
var refKeyword = isLocalOrField && !isConst && this.CurrentToken.Kind == SyntaxKind.RefKeyword && !this.IsPossibleLambdaExpression(Precedence.Expression)
? this.EatToken()
: null;
var equals = this.EatToken();

var init = this.ParseVariableInitializer();
initializer = _syntaxFactory.EqualsValueClause(
equals,
refKeyword == null ? init : _syntaxFactory.RefExpression(refKeyword, init));
break;
// check for lambda expression with explicit ref return type: `ref int () => { ... }`
var refKeyword = isLocalOrField && !isConst && this.CurrentToken.Kind == SyntaxKind.RefKeyword && !this.IsPossibleLambdaExpression(Precedence.Expression)
? this.EatToken()
: null;

case SyntaxKind.LessThanToken:
if (allowLocalFunctions && isFirst)
{
localFunction = TryParseLocalFunctionStatementBody(attributes, mods, parentType, name);
if (localFunction != null)
{
return null;
}
}
goto default;
var init = this.ParseVariableInitializer();
var initializer = _syntaxFactory.EqualsValueClause(
equals,
refKeyword == null ? init : _syntaxFactory.RefExpression(refKeyword, init));

case SyntaxKind.OpenParenToken:
if (allowLocalFunctions && isFirst)
localFunction = null;
return _syntaxFactory.VariableDeclarator(name, argumentList, initializer);
}

VariableDeclaratorSyntax parseVariableDeclaratorWithLessThanToken(SyntaxToken name, out LocalFunctionStatementSyntax localFunction)
{
if (allowLocalFunctions && isFirst)
{
Debug.Assert(!isFixed, "Both the fixed-size-buffer and fixed-statement codepaths pass through allowLocalFunctions=false");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added some asserts to help reason about things as well.

localFunction = TryParseLocalFunctionStatementBody(attributes, mods, parentType, name);
if (localFunction != null)
{
localFunction = TryParseLocalFunctionStatementBody(attributes, mods, parentType, name);
if (localFunction != null)
{
return null;
}
return null;
}
}

// Special case for accidental use of C-style constructors
// Fake up something to hold the arguments.
_termState |= TerminatorState.IsPossibleEndOfVariableDeclaration;
argumentList = this.ParseBracketedArgumentList();
_termState = saveTerm;
argumentList = this.AddError(argumentList, ErrorCode.ERR_BadVarDecl);
break;
return parseVariableDeclaratorDefault(name, out localFunction);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a goto now just becomes a simple recursive call.

}

case SyntaxKind.OpenBracketToken:
bool sawNonOmittedSize;
_termState |= TerminatorState.IsPossibleEndOfVariableDeclaration;
var specifier = this.ParseArrayRankSpecifier(sawNonOmittedSize: out sawNonOmittedSize);
_termState = saveTerm;
var open = specifier.OpenBracketToken;
var sizes = specifier.Sizes;
var close = specifier.CloseBracketToken;
if (isFixed && !sawNonOmittedSize)
VariableDeclaratorSyntax parseVariableDeclaratorWithOpenParenToken(
SyntaxToken name, out LocalFunctionStatementSyntax localFunction)
{
if (allowLocalFunctions && isFirst)
{
Debug.Assert(!isFixed, "Both the fixed-size-buffer and fixed-statement codepaths pass through allowLocalFunctions=false");
localFunction = TryParseLocalFunctionStatementBody(attributes, mods, parentType, name);
if (localFunction != null)
{
close = this.AddError(close, ErrorCode.ERR_ValueExpected);
return null;
}
}

var args = _pool.AllocateSeparated<ArgumentSyntax>();
var withSeps = sizes.GetWithSeparators();
foreach (var item in withSeps)
{
if (item is ExpressionSyntax expression)
{
bool isOmitted = expression.Kind == SyntaxKind.OmittedArraySizeExpression;
if (!isFixed && !isOmitted)
{
expression = this.AddError(expression, ErrorCode.ERR_ArraySizeInDeclaration);
}
// Special case for accidental use of C-style constructors
// Fake up something to hold the arguments.
var saveTerm = _termState;
_termState |= TerminatorState.IsPossibleEndOfVariableDeclaration;
var argumentList = this.ParseBracketedArgumentList();
_termState = saveTerm;
argumentList = this.AddError(argumentList, ErrorCode.ERR_BadVarDecl);

args.Add(_syntaxFactory.Argument(null, refKindKeyword: null, expression));
}
else
{
args.AddSeparator((SyntaxToken)item);
}
}
localFunction = null;
return _syntaxFactory.VariableDeclarator(name, argumentList, initializer: null);
}

argumentList = _syntaxFactory.BracketedArgumentList(open, _pool.ToListAndFree(args), close);
if (!isFixed)
VariableDeclaratorSyntax parseVariableDeclaratorWithOpenBracketToken(
SyntaxToken name, out LocalFunctionStatementSyntax localFunction)
{
var saveTerm = _termState;
_termState |= TerminatorState.IsPossibleEndOfVariableDeclaration;
var specifier = this.ParseArrayRankSpecifier(sawNonOmittedSize: out var sawNonOmittedSize);
_termState = saveTerm;
var open = specifier.OpenBracketToken;
var sizes = specifier.Sizes;
var close = specifier.CloseBracketToken;
if (isFixed && !sawNonOmittedSize)
{
close = this.AddError(close, ErrorCode.ERR_ValueExpected);
}

var args = _pool.AllocateSeparated<ArgumentSyntax>();
var withSeps = specifier.Sizes.GetWithSeparators();
foreach (var item in withSeps)
{
if (item is ExpressionSyntax expression)
{
argumentList = this.AddError(argumentList, ErrorCode.ERR_CStyleArray);
// If we have "int x[] = new int[10];" then parse the initializer.
if (this.CurrentToken.Kind == SyntaxKind.EqualsToken)
bool isOmitted = expression.Kind == SyntaxKind.OmittedArraySizeExpression;
if (!isFixed && !isOmitted)
{
goto case SyntaxKind.EqualsToken;
expression = this.AddError(expression, ErrorCode.ERR_ArraySizeInDeclaration);
}

args.Add(_syntaxFactory.Argument(null, refKindKeyword: null, expression));
}
else
{
args.AddSeparator((SyntaxToken)item);
}
}

break;
var argumentList = _syntaxFactory.BracketedArgumentList(open, _pool.ToListAndFree(args), close);
if (!isFixed)
{
argumentList = this.AddError(argumentList, ErrorCode.ERR_CStyleArray);
// If we have "int x[] = new int[10];" then parse the initializer.
if (this.CurrentToken.Kind == SyntaxKind.EqualsToken)
return parseNonFixedVariableDeclaratorWithEqualsToken(name, argumentList, out localFunction);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can now safely do this recursion, without worrying about jumping to the 'default' branch because we know we're not fixed (!isFixed) above, and we know we have an = token, so we can safely jump to that helper with those invariants.

}

default:
if (isConst)
localFunction = null;
return _syntaxFactory.VariableDeclarator(name, argumentList, initializer: null);
}

VariableDeclaratorSyntax parseVariableDeclaratorDefault(
SyntaxToken name, out LocalFunctionStatementSyntax localFunction)
{
// Note: it is ok that we do this work prior to the isConst/isFixed checks below. If it looks like
// a variable initializer, that means we're missing at least an equals and we'll report that error
// here. So it's fine to not report the other errors related to const/fixed as they can be fixed up
// once the user adds the '='.
if (looksLikeVariableInitializer())
{
localFunction = null;
return _syntaxFactory.VariableDeclarator(
name,
argumentList: null,
_syntaxFactory.EqualsValueClause(
this.EatToken(SyntaxKind.EqualsToken),
this.ParseVariableInitializer()));
}

if (isConst)
{
// Error here for missing constant initializers. Note: this error would be better suited in the
// binder as we do not need to make an syntax tree with diagnostics here.
name = this.AddError(name, ErrorCode.ERR_ConstValueRequired);
}
else if (isFixed)
{
if (parentType.Kind == SyntaxKind.ArrayType)
{
name = this.AddError(name, ErrorCode.ERR_ConstValueRequired); // Error here for missing constant initializers
// They accidentally put the array before the identifier
name = this.AddError(name, ErrorCode.ERR_FixedDimsRequired);
}
else if (isFixed)
else
{
if (parentType.Kind == SyntaxKind.ArrayType)
{
// They accidentally put the array before the identifier
name = this.AddError(name, ErrorCode.ERR_FixedDimsRequired);
}
else
{
goto case SyntaxKind.OpenBracketToken;
}
return parseVariableDeclaratorWithOpenBracketToken(name, out localFunction);
}
}

break;
localFunction = null;
return _syntaxFactory.VariableDeclarator(name, argumentList: null, initializer: null);
}

localFunction = null;
return _syntaxFactory.VariableDeclarator(name, argumentList, initializer);
bool looksLikeVariableInitializer()
{
// Note: this check is redundant, as CanStartExpression will return false for an equals-token. However,
// we want to guarantee that this always holds true, and thus the caller will *always* report an error
// when trying to consume the equals token. That ensures that we it's then ok to skip other syntax
// errors that are reported with variable declarators.
if (this.CurrentToken.Kind == SyntaxKind.EqualsToken)
return false;

// If we see a token that can start an expression after the identifier (e.g., "int value 5;"),
// treat it as a missing '=' and parse the initializer.
//
// Do this except for cases that are better served by saying we have a missing comma. Specifically:
//
// Type t1 t2 t3
// Type t1 t2,
// Type t1 t2 = ...
// Type t1 t2;
// Type t1 t2) // likely an incorrect tuple.
var shouldParseAsNextDeclarator =
this.CurrentToken.Kind == SyntaxKind.IdentifierToken &&
this.PeekToken(1).Kind is SyntaxKind.IdentifierToken or SyntaxKind.CommaToken or SyntaxKind.EqualsToken or SyntaxKind.SemicolonToken or SyntaxKind.CloseParenToken or SyntaxKind.EndOfFileToken;
if (shouldParseAsNextDeclarator)
return false;

if (ContainsErrorDiagnostic(name))
return false;

if (!CanStartExpression())
return false;

using var _ = this.GetDisposableResetPoint(resetOnDispose: true);
var initializer = this.ParseExpressionCore();

// If we see a type following, then prefer to view this as a declarator for the next variable.
if (initializer is TypeSyntax)
return false;

return !ContainsErrorDiagnostic(initializer);
}
}

// Is there a local function after an eaten identifier?
Expand Down
19 changes: 12 additions & 7 deletions src/Compilers/CSharp/Test/Semantic/Semantics/RefFieldTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22156,6 +22156,9 @@ void M()
}";
comp = CreateCompilation(source);
comp.VerifyDiagnostics(
// (1,12): error CS8983: A 'struct' with field initializers must include an explicitly declared constructor.
// ref struct R
Diagnostic(ErrorCode.ERR_StructHasInitializersAndNoDeclaredConstructor, "R").WithLocation(1, 12),
// (3,9): error CS0246: The type or namespace name 'scoped' could not be found (are you missing a using directive or an assembly reference?)
// ref scoped R M() => throw null;
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "scoped").WithArguments("scoped").WithLocation(3, 9),
Expand All @@ -22168,16 +22171,18 @@ void M()
// (3,16): warning CS0169: The field 'R.R' is never used
// ref scoped R M() => throw null;
Diagnostic(ErrorCode.WRN_UnreferencedField, "R").WithArguments("R.R").WithLocation(3, 16),
// (3,18): error CS1002: ; expected
// (3,18): error CS1003: Syntax error, '=' expected
// ref scoped R M() => throw null;
Diagnostic(ErrorCode.ERR_SemicolonExpected, "M").WithLocation(3, 18),
// (3,18): error CS1520: Method must have a return type
Diagnostic(ErrorCode.ERR_SyntaxError, "M").WithArguments("=").WithLocation(3, 18),
// (3,18): error CS8172: Cannot initialize a by-reference variable with a value
// ref scoped R M() => throw null;
Diagnostic(ErrorCode.ERR_MemberNeedsType, "M").WithLocation(3, 18),
// (3,18): error CS8958: The parameterless struct constructor must be 'public'.
Diagnostic(ErrorCode.ERR_InitializeByReferenceVariableWithValue, "M() => throw null").WithLocation(3, 18),
// (3,18): error CS0246: The type or namespace name 'M' could not be found (are you missing a using directive or an assembly reference?)
// ref scoped R M() => throw null;
Diagnostic(ErrorCode.ERR_NonPublicParameterlessStructConstructor, "M").WithLocation(3, 18)
);
Diagnostic(ErrorCode.ERR_SingleTypeNameNotFound, "M").WithArguments("M").WithLocation(3, 18),
// (3,18): error CS1510: A ref or out value must be an assignable variable
// ref scoped R M() => throw null;
Diagnostic(ErrorCode.ERR_RefLvalueExpected, "M() => throw null").WithLocation(3, 18));

source = @"
delegate void M(ref scoped R parameter);
Expand Down
Loading
Loading