Skip to content

Improve error recovery for => in constructor initializers#80764

Merged
CyrusNajmabadi merged 10 commits intomainfrom
copilot/better-quickinfo-for-this-base
Nov 3, 2025
Merged

Improve error recovery for => in constructor initializers#80764
CyrusNajmabadi merged 10 commits intomainfrom
copilot/better-quickinfo-for-this-base

Conversation

Copy link
Contributor

Copilot AI commented Oct 16, 2025

Problem

When users mistakenly write public MyClass() => base() instead of the correct syntax public MyClass() : base(), the C# compiler produced a cascade of 7+ confusing error messages that didn't clearly indicate the actual problem:

public class MyClass : MyBaseClass
{
    public MyClass() => base() {}  // Wrong: using => instead of :
}

Previous error messages:

  • error CS1002: ; expected
  • error CS1519: Invalid token '{' in a member declaration
  • error CS8803: Top-level statements must precede namespace and type declarations
  • error CS0106: The modifier 'public' is not valid for this item
  • error CS1001: Identifier expected
  • error CS1002: ; expected
  • error CS1022: Type or namespace definition, or end-of-file expected

These errors were confusing because they suggested problems with top-level statements, modifiers, and type declarations, when the actual issue was simply using the wrong token before the constructor initializer.

Solution

Following the pattern established in PR #80695 for similar error recovery scenarios, the parser now:

  1. Detects the pattern: When it sees => followed by base( or this( (with open parenthesis)
  2. Recovers gracefully: Uses EatTokenAsKind to consume the => token as if it were a :, attaching it as skipped syntax
  3. Produces a clear error: Generates a single, focused error message pointing to the exact problem
  4. Continues parsing: Parses the constructor initializer normally, preventing cascading errors
  5. Does not interfere with legal syntax: Legal expression-bodied constructors like => this.Goo(), => this[0], => base.Goo(), and => base[0] are parsed correctly as expression bodies

Changes

Modified ParseConstructorDeclaration in LanguageParser.cs:

  • Added lookahead pattern matching to detect => base( or => this( (specifically with open parenthesis)
  • Used EatTokenAsKind(SyntaxKind.ColonToken) to consume the errant => with proper error reporting
  • Manually parsed the constructor initializer to ensure correct syntax tree structure

Added comprehensive tests in DeclarationParsingTests.cs:

  • Tests use raw string literals for cleaner, more modern syntax
  • Error recovery tests for both base() and this():
    • Basic error cases: C() => base() and C() => this()
    • With arguments: C() => base(1) { } and C() => this(1) { }
    • With no body: C() => base(1) and C() => this(1)
    • With semicolon: C() => base(1); and C() => this(1);
    • With arrow expression body: C() => base(1) => Console.WriteLine(); and C() => this(1) => Console.WriteLine();
  • Legal expression-bodied constructor tests to ensure no interference:
    • C() => this[0] = 1; (indexer access)
    • C() => this.Goo(); (member access)
    • C() => base.Goo(); (base member access)
    • C() => base[0] = 1; (base indexer access)

Result

New error message:

  • error CS1003: Syntax error, ':' expected

This single, clear error message immediately tells the developer exactly what's wrong and where.

Visual Demonstration

Testing

  • ✅ All 14 constructor initializer tests pass
  • ✅ All existing parsing tests pass
  • ✅ Edge cases verified: only base( and this( trigger error recovery, not base., this., base[, or this[
  • ✅ No regressions introduced

Fixes #40879

Original prompt

This section details on the original issue you should resolve

<issue_title>Better QuickInfo for "this" and "base" in combination with => operator</issue_title>
<issue_description>With the modern short method notation, I was ran into the problem that a constructor don't accept a => operator in combination with this and base calls.

For this situation, it would be nicer when the quick info offer a better info about the wrong usage.

    public class MyBaseClass
    {
        public MyBaseClass() {}
    }

    public sealed class MyClass : MyBaseClass
    {
        // Original: The usage of base-keyword is not valid in this context
        // Better: can't use base-keyword with => operator did you mean ": base()" - call
        public MyClass() => base() {}

        // Original: method name expected
        // Better: can't use this-keyword with => operator did you mean ": this()" - call
        public MyClass() => this() {}
    }

Version Used:
Visual Studio Community 2019
Version 16.5.0 Preview 1.0</issue_description>

<agent_instructions>Copilot, using a similar fix to the one you've made in #80695 please detect and gracefully recover from an errant => at the start of a constructor initializer. </agent_instructions>

Comments on the Issue (you are @copilot in this section)

@CyrusNajmabadi ![image](https://user-images.githubusercontent.com/4564579/72175768-c9f87800-3391-11ea-8286-5e9873f39f72.png)

?</comment_new>

Fixes #40879


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
Copilot AI changed the title [WIP] Improve QuickInfo messages for 'this' and 'base' with => operator Improve error recovery for => in constructor initializers Oct 16, 2025
Copilot AI requested a review from CyrusNajmabadi October 16, 2025 16:03
[Fact, WorkItem("https://github.com/dotnet/roslyn/issues/40879")]
public void ConstructorInitializerWithArrowInsteadOfColon_Base()
{
UsingTree(@"
Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot use raw string literals for these tests. Also, add tests that do things like => this[0]; and this.Goo() and the same for the base case. We want to ensure that we only detect this problem if you have this( and base( and not the start of some legal expression that starts with this/base. If there are other reasonable constructs to test, add those as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated tests to use raw string literals and added edge case tests for => this[0], => this.Goo(), => base.Goo(), and => base[0] to ensure these legal expression-bodied constructors are not treated as errors. All tests pass. Changes in commit 0c7c8e6.

CyrusNajmabadi and others added 2 commits October 16, 2025 18:23
…alizers

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
Copilot AI requested a review from CyrusNajmabadi October 16, 2025 16:30
@CyrusNajmabadi CyrusNajmabadi marked this pull request as ready for review October 16, 2025 18:44
@CyrusNajmabadi CyrusNajmabadi requested a review from a team as a code owner October 16, 2025 18:44
@CyrusNajmabadi
Copy link
Contributor

@dotnet/roslyn-compiler ptal.

}
private ConstructorInitializerSyntax ParseConstructorInitializer()
{
var colon = this.EatTokenAsKind(SyntaxKind.ColonToken);
Copy link
Member

Choose a reason for hiding this comment

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

There are several other callers of ParseConstructorInitializer. Should all of them be consuming the current token as a : for error recovery?

Copy link
Contributor

Choose a reason for hiding this comment

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

i'm fine not touching the other caller. I think this is an appropriate fix to address the case of someone wanting ot write the constructor initializer, because they know they're in a constructor, but misuse the wrong leading cahr.

Note: the reason for detecting the this/base initialier on a method is to have better error recovery around when constructors are accidentally not written as constructors (like accidetnally writing 'void'). The check for the initializer means we don't go off the rails there.

I don't think we to both be resilient to having an errant return type and a this/base written with =>.

Copy link
Member

Choose a reason for hiding this comment

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

I'm more concerned about the behavior change here. EatToken(kind) does not consume the current token if it doesn't match, and EatTokenAsKind does. We should at least investigate potential behavioral changes there.

Copy link
Contributor

Choose a reason for hiding this comment

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

Added assert and comment in 930cbd4 to help explain what's going on.

UsingTree("""
class C
{
C() => base() { }
Copy link
Member

Choose a reason for hiding this comment

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

Let's add some more variations on this. For example, C() => base(1);. Or C() => base(1) => Console.Write(1);.

Copy link
Member

Choose a reason for hiding this comment

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

And same for the this tests below. Could we factor these two into a theory?

Copy link
Contributor

Choose a reason for hiding this comment

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

my preference with theories is they not then contain conditional logic within them to specify the expected outcomes (as i find it hard to mentally then template instantiate each theory out in my head). I'm good with individual tests that overlap by a lot.

Copy link
Contributor

Choose a reason for hiding this comment

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

@CyrusNajmabadi
Copy link
Contributor

@jjonescz can you ptal?

@RikkiGibson RikkiGibson self-requested a review November 3, 2025 20:13
@RikkiGibson
Copy link
Member

Will try to get a review in this afternoon

UsingTree("""
class C
{
C() => base(1) => Console.WriteLine();
Copy link
Member

Choose a reason for hiding this comment

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

Would also be good to see behavior of cases like

  • C() => base(1) (no body at all)
  • C() => base(1); (semicolon body. This would be an error case for partial constructor definition part, I guess.)

Copy link
Contributor

Choose a reason for hiding this comment

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

@copilot can you please add tests for the above two cases?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added tests for both cases:

  • C() => base(1) (no body at all)
  • C() => base(1); (semicolon body)

Also added corresponding tests for this constructor initializers for consistency. All tests pass. Changes in commit 9c8f2a3.

Co-authored-by: CyrusNajmabadi <4564579+CyrusNajmabadi@users.noreply.github.com>
@CyrusNajmabadi CyrusNajmabadi enabled auto-merge (squash) November 3, 2025 22:52
@CyrusNajmabadi CyrusNajmabadi merged commit f20ab1d into main Nov 3, 2025
23 of 24 checks passed
@dotnet-policy-service dotnet-policy-service bot added this to the Next milestone Nov 3, 2025
@CyrusNajmabadi CyrusNajmabadi deleted the copilot/better-quickinfo-for-this-base branch November 4, 2025 00:13
@davidwengier davidwengier modified the milestones: Next, 18.3 Jan 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Better QuickInfo for "this" and "base" in combination with => operator

5 participants