Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Field-backed properties: recognize field in interpolated string #75566

Merged
merged 5 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ CodeAnalysis.Syntax.InternalSyntax.SyntaxList<InterpolatedStringContentSyntax> g
originalTextSpan[currentContentStart..interpolation.OpenBraceRange.Start]));

// Now parse the interpolation itself.
var interpolationNode = ParseInterpolation(this.Options, originalText, interpolation, kind);
var interpolationNode = ParseInterpolation(this.Options, originalText, interpolation, kind, IsInFieldKeywordContext);

// Make sure the interpolation starts at the right location.
var indentationError = getInterpolationIndentationError(indentationWhitespace, interpolation);
Expand Down Expand Up @@ -361,7 +361,8 @@ private static InterpolationSyntax ParseInterpolation(
CSharpParseOptions options,
string text,
Lexer.Interpolation interpolation,
Lexer.InterpolatedStringKind kind)
Lexer.InterpolatedStringKind kind,
bool isInFieldKeywordContext)
{
// Grab the text from after the { all the way to the start of the } (or the start of the : if present). This
// will be used to parse out the expression of the interpolation.
Expand All @@ -377,6 +378,7 @@ private static InterpolationSyntax ParseInterpolation(

// Now create a parser to actually handle the expression portion of the interpolation
using var tempParser = new LanguageParser(tempLexer, oldTree: null, changes: null);
using var _ = new FieldKeywordContext(tempParser, isInFieldKeywordContext);

var result = tempParser.ParseInterpolation(
text, interpolation, kind,
Expand Down
293 changes: 293 additions & 0 deletions src/Compilers/CSharp/Test/Emit3/FieldKeywordTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10171,6 +10171,299 @@ partial class B
VerifyMergedProperties(actualProperties, actualFields);
}

[Theory]
[CombinatorialData]
public void InterpolatedString(
[CombinatorialValues(LanguageVersion.CSharp13, LanguageVersionFacts.CSharpNext)] LanguageVersion languageVersion)
{
string source = $$"""
using System;
class C
{
public object P1 => $"P1: {field is null}";
public object P2 { get; set { field = value; field = $"{field}"; } }
}
class Program
{
static void Main()
{
var c = new C();
c.P2 = 2;
Console.WriteLine(c.P1);
Console.WriteLine(c.P2);
}
}
""";
var comp = CreateCompilation(
source,
parseOptions: TestOptions.Regular.WithLanguageVersion(languageVersion),
options: TestOptions.ReleaseExe,
targetFramework: TargetFramework.Net80);

if (languageVersion == LanguageVersion.CSharp13)
{
comp.VerifyEmitDiagnostics(
// (4,32): error CS0103: The name 'field' does not exist in the current context
// public object P1 => $"P1: {field is null}";
Diagnostic(ErrorCode.ERR_NameNotInContext, "field").WithArguments("field").WithLocation(4, 32),
// (5,19): error CS8652: The feature 'field keyword' is currently in Preview and *unsupported*. To use Preview features, use the 'preview' language version.
// public object P2 { get; set { field = value; field = $"{field}"; } }
Diagnostic(ErrorCode.ERR_FeatureInPreview, "P2").WithArguments("field keyword").WithLocation(5, 19),
// (5,35): error CS0103: The name 'field' does not exist in the current context
// public object P2 { get; set { field = value; field = $"{field}"; } }
Diagnostic(ErrorCode.ERR_NameNotInContext, "field").WithArguments("field").WithLocation(5, 35),
// (5,50): error CS0103: The name 'field' does not exist in the current context
// public object P2 { get; set { field = value; field = $"{field}"; } }
Diagnostic(ErrorCode.ERR_NameNotInContext, "field").WithArguments("field").WithLocation(5, 50),
// (5,61): error CS0103: The name 'field' does not exist in the current context
// public object P2 { get; set { field = value; field = $"{field}"; } }
Diagnostic(ErrorCode.ERR_NameNotInContext, "field").WithArguments("field").WithLocation(5, 61));
}
else
{
var verifier = CompileAndVerify(comp, verify: Verification.Skipped, expectedOutput: IncludeExpectedOutput("""
P1: True
2
"""));
Copy link
Member

Choose a reason for hiding this comment

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

Consider calling verifier.VerifyDiagnostics. Applies to the tests below as well.

verifier.VerifyDiagnostics();
verifier.VerifyIL("C.P1.get", """
{
// Code size 45 (0x2d)
.maxstack 3
.locals init (System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_0)
IL_0000: ldloca.s V_0
IL_0002: ldc.i4.4
IL_0003: ldc.i4.1
IL_0004: call "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"
IL_0009: ldloca.s V_0
IL_000b: ldstr "P1: "
IL_0010: call "void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendLiteral(string)"
IL_0015: ldloca.s V_0
IL_0017: ldarg.0
IL_0018: ldfld "object C.<P1>k__BackingField"
IL_001d: ldnull
IL_001e: ceq
IL_0020: call "void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted<bool>(bool)"
IL_0025: ldloca.s V_0
IL_0027: call "string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"
IL_002c: ret
}
""");
verifier.VerifyIL("C.P2.set", """
{
// Code size 43 (0x2b)
.maxstack 4
.locals init (System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_0)
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: stfld "object C.<P2>k__BackingField"
IL_0007: ldarg.0
IL_0008: ldloca.s V_0
IL_000a: ldc.i4.0
IL_000b: ldc.i4.1
IL_000c: call "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"
IL_0011: ldloca.s V_0
IL_0013: ldarg.0
IL_0014: ldfld "object C.<P2>k__BackingField"
IL_0019: call "void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted<object>(object)"
IL_001e: ldloca.s V_0
IL_0020: call "string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"
IL_0025: stfld "object C.<P2>k__BackingField"
IL_002a: ret
}
""");
}
}

[Theory]
[CombinatorialData]
public void InterpolatedString_Alignment(
[CombinatorialValues(LanguageVersion.CSharp13, LanguageVersionFacts.CSharpNext)] LanguageVersion languageVersion)
{
string source = $$"""
using System;
class C
{
int x = 42;
const int field = 5;
public int P1 { get { Console.WriteLine($"{x,field}"); return 1; } }
}
class Program
{
static void Main()
{
var c = new C();
Console.WriteLine(c.P1);
}
}
""";
var comp = CreateCompilation(
source,
parseOptions: TestOptions.Regular.WithLanguageVersion(languageVersion),
options: TestOptions.ReleaseExe,
targetFramework: TargetFramework.Net80);

if (languageVersion == LanguageVersion.CSharp13)
{
var verifier = CompileAndVerify(comp, verify: Verification.Skipped, expectedOutput: IncludeExpectedOutput("""
42
1
"""));
verifier.VerifyDiagnostics();
verifier.VerifyIL("C.P1.get", """
{
// Code size 37 (0x25)
.maxstack 3
.locals init (System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_0)
IL_0000: ldloca.s V_0
IL_0002: ldc.i4.0
IL_0003: ldc.i4.1
IL_0004: call "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"
IL_0009: ldloca.s V_0
IL_000b: ldarg.0
IL_000c: ldfld "int C.x"
IL_0011: ldc.i4.5
IL_0012: call "void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted<int>(int, int)"
IL_0017: ldloca.s V_0
IL_0019: call "string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"
IL_001e: call "void System.Console.WriteLine(string)"
IL_0023: ldc.i4.1
IL_0024: ret
}
""");
}
else
{
comp.VerifyEmitDiagnostics(
// (6,50): warning CS9258: In language version preview, the 'field' keyword binds to a synthesized backing field for the property. To avoid generating a synthesized backing field, and to refer to the existing member, use 'this.field' or '@field' instead.
// public int P1 { get { Console.WriteLine($"{x,field}"); return 1; } }
Diagnostic(ErrorCode.WRN_FieldIsAmbiguous, "field").WithArguments("preview").WithLocation(6, 50),
// (6,50): error CS0150: A constant value is expected
// public int P1 { get { Console.WriteLine($"{x,field}"); return 1; } }
Diagnostic(ErrorCode.ERR_ConstantExpected, "field").WithLocation(6, 50));
}

var containingType = comp.GetMember<NamedTypeSymbol>("C");
var actualFields = containingType.GetMembers().OfType<FieldSymbol>().Where(f => f.IsImplicitlyDeclared).ToImmutableArray();
if (languageVersion == LanguageVersion.CSharp13)
{
Assert.Empty(actualFields.ToTestDisplayStrings());
}
else
{
var expectedFields = new[]
{
"System.Int32 C.<P1>k__BackingField",
};
AssertEx.Equal(expectedFields, actualFields.ToTestDisplayStrings());
}

var actualProperties = containingType.GetMembers().OfType<PropertySymbol>().ToImmutableArray();
Assert.Equal(1, actualProperties.Length);
if (languageVersion == LanguageVersion.CSharp13)
{
Assert.True(actualProperties[0] is SourcePropertySymbol { Name: "P1", IsAutoProperty: false, UsesFieldKeyword: false, BackingField: null });
}
else
{
Assert.True(actualProperties[0] is SourcePropertySymbol { Name: "P1", IsAutoProperty: false, UsesFieldKeyword: true, BackingField: { } });
}

VerifyMergedProperties(actualProperties, actualFields);
}

[Theory]
[CombinatorialData]
public void InterpolatedString_Format(
[CombinatorialValues(LanguageVersion.CSharp13, LanguageVersionFacts.CSharpNext)] LanguageVersion languageVersion)
{
string source = $$"""
using System;
class C
{
int x = 42;
const int y = 3;
const int field = 5;
public int P2 { get { Console.WriteLine($"{x:field}"); return 2; } }
public int P3 { get { Console.WriteLine($"{x,y:field}"); return 3; } }
}
class Program
{
static void Main()
{
var c = new C();
Console.WriteLine((c.P2, c.P3));
}
}
""";
var comp = CreateCompilation(
source,
parseOptions: TestOptions.Regular.WithLanguageVersion(languageVersion),
options: TestOptions.ReleaseExe,
targetFramework: TargetFramework.Net80);

var verifier = CompileAndVerify(comp, verify: Verification.Skipped, expectedOutput: IncludeExpectedOutput("""
field
field
(2, 3)
"""));
verifier.VerifyDiagnostics();
verifier.VerifyIL("C.P2.get", """
{
// Code size 41 (0x29)
.maxstack 3
.locals init (System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_0)
IL_0000: ldloca.s V_0
IL_0002: ldc.i4.0
IL_0003: ldc.i4.1
IL_0004: call "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"
IL_0009: ldloca.s V_0
IL_000b: ldarg.0
IL_000c: ldfld "int C.x"
IL_0011: ldstr "field"
IL_0016: call "void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted<int>(int, string)"
IL_001b: ldloca.s V_0
IL_001d: call "string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"
IL_0022: call "void System.Console.WriteLine(string)"
IL_0027: ldc.i4.2
IL_0028: ret
}
""");
verifier.VerifyIL("C.P3.get", """
{
// Code size 42 (0x2a)
.maxstack 4
.locals init (System.Runtime.CompilerServices.DefaultInterpolatedStringHandler V_0)
IL_0000: ldloca.s V_0
IL_0002: ldc.i4.0
IL_0003: ldc.i4.1
IL_0004: call "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler..ctor(int, int)"
IL_0009: ldloca.s V_0
IL_000b: ldarg.0
IL_000c: ldfld "int C.x"
IL_0011: ldc.i4.3
IL_0012: ldstr "field"
IL_0017: call "void System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.AppendFormatted<int>(int, int, string)"
IL_001c: ldloca.s V_0
IL_001e: call "string System.Runtime.CompilerServices.DefaultInterpolatedStringHandler.ToStringAndClear()"
IL_0023: call "void System.Console.WriteLine(string)"
IL_0028: ldc.i4.3
IL_0029: ret
}
""");

var containingType = comp.GetMember<NamedTypeSymbol>("C");
var actualFields = containingType.GetMembers().OfType<FieldSymbol>().Where(f => f.IsImplicitlyDeclared).ToImmutableArray();
Assert.Empty(actualFields.ToTestDisplayStrings());

var actualProperties = containingType.GetMembers().OfType<PropertySymbol>().ToImmutableArray();
Assert.Equal(2, actualProperties.Length);
Assert.True(actualProperties[0] is SourcePropertySymbol { Name: "P2", IsAutoProperty: false, UsesFieldKeyword: false, BackingField: null });
Assert.True(actualProperties[1] is SourcePropertySymbol { Name: "P3", IsAutoProperty: false, UsesFieldKeyword: false, BackingField: null });

VerifyMergedProperties(actualProperties, actualFields);
}

[Theory]
[InlineData("{ get; }")]
[InlineData("{ get; set; }")]
Expand Down
Loading