Skip to content

InterpolatedStringHandlerArgumentAttribute("") is not correctly taken into account when the receiver is a struct #58514

@shkdee

Description

@shkdee

Version Used:
Visual Studio v17.0.4

SDK .NET (reflétant tous les fichiers global.json) :
 Version:   6.0.101
 Commit:    ef49f6213a

Environnement d'exécution :
 OS Name:     Windows
 OS Version:  10.0.19044
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\6.0.101\

Host (useful for support):
  Version: 6.0.1
  Commit:  3a25a7f1cc

Steps to Reproduce:

The issue occurs when a you define a method on struct, that expects to receive an InterpolatedStringHandler, and when you specify [InterpolatedStringHandlerArgument("")] for the arguments:

internal struct StructLogger
{
    ....
    public void Log([InterpolatedStringHandlerArgument("")] DummyHandler handler) => ....;
    ....
}

[InterpolatedStringHandler]
internal ref struct DummyHandler
{
    ...
    public DummyHandler(int literalLength, int formattedCount, StructLogger logger, out bool enabled)
    {
        // here, logger contains either the default value for StructLogger, or possibly another value than the expected one
    }
}

Basically, depending on how you call StructLogger.Log and on how you compiled the code (!), the value logger in the DummyHandler constructor can be either the default value for StructLogger, or a previously created value, but not the one on which you actually called Log($"..."). I believe this is a compiler issue, because of the behavior difference between debug and release mode, and because the issue clearly looks to be present in the IL code when you decompile the generated dll (but I am less familiar with that so I won't try to point it out directly).

I wrote a test case to demonstrate the issue, you just have to compile the code as a single file console project and run it. It is enclosed at the bottom of this message. It is also interesting to note that the behavior is not the same in debug and release.

Expected Behavior:
Output the following:

Start
Creating DummyHandler from StructLogger#1
StructLogger#1:
(1) i=0
Creating DummyHandler from StructLogger#2
StructLogger#2:
(2) i=0
Creating DummyHandler from ClassLogger#3
ClassLogger#3:
(3) i=0
Creating DummyHandler from ClassLogger#4
ClassLogger#4:
(4) i=0
End

Actual Behavior:
In debug, you get:

Start
Creating DummyHandler from StructLogger#0
StructLogger#1: log:0
(1) i=1
Creating DummyHandler from StructLogger#0
StructLogger#2: log:1
(2) i=2
Creating DummyHandler from ClassLogger#3
ClassLogger#3:
(3) i=2
Creating DummyHandler from ClassLogger#4
ClassLogger#4:
(4) i=2
End

And in release, you get (note the discrepancy between the logger id passed to the DummyHandler constructor and the one actually used to issue the log line):

Start
Creating DummyHandler from StructLogger#0
StructLogger#1: log:0
(1) i=1
Creating DummyHandler from StructLogger#1
StructLogger#2:
(2) i=1
Creating DummyHandler from ClassLogger#3
ClassLogger#3:
(3) i=1
Creating DummyHandler from ClassLogger#4
ClassLogger#4:
(4) i=1
End

Test case file content

using System.Runtime.CompilerServices;
using System.Text;

Console.WriteLine("Start");
var i = 0;
new StructLogger(true, 1).Log($"log:{i++}");
Console.WriteLine($"(1) i={i}");
var s = new StructLogger(true, 2);
s.Log($"log:{i++}");
Console.WriteLine($"(2) i={i}");
new ClassLogger(true, 3).Log($"log:{i++}");
Console.WriteLine($"(3) i={i}");
var c = new ClassLogger(true, 4);
c.Log($"log:{i++}");
Console.WriteLine($"(4) i={i}");
Console.WriteLine("End");

internal readonly struct StructLogger
{
    private readonly bool _disabled;
    private readonly int _id;

    public bool Disabled => _disabled;
    public int Id => _id;

    public StructLogger(bool disabled, int id)
    {
        _disabled = disabled;
        _id = id;
    }

    public void Log([InterpolatedStringHandlerArgument("")] DummyHandler handler) => Console.WriteLine($"StructLogger#{_id}: " + handler.GetContent());
}

// Include a class-based logger to show what should happen
internal class ClassLogger
{
    public bool Disabled { get; }
    public int Id { get; }

    public ClassLogger(bool disabled, int id)
    {
        Disabled = disabled;
        Id = id;
    }

    public void Log([InterpolatedStringHandlerArgument("")] DummyHandler handler) => Console.WriteLine($"ClassLogger#{Id}: " + handler.GetContent());
}

[InterpolatedStringHandler]
internal ref struct DummyHandler
{
    private readonly StringBuilder? _builder;
    public DummyHandler(int literalLength, int formattedCount, StructLogger structLogger, out bool enabled)
    {
        Console.WriteLine($"Creating DummyHandler from StructLogger#{structLogger.Id}");
        enabled = !structLogger.Disabled;
        _builder = structLogger.Disabled ? null : new StringBuilder();
    }
    public DummyHandler(int literalLength, int formattedCount, ClassLogger classLogger, out bool enabled)
    {
        Console.WriteLine($"Creating DummyHandler from ClassLogger#{classLogger.Id}");
        enabled = !classLogger.Disabled;
        _builder = classLogger.Disabled ? null : new StringBuilder();
    }

    public string? GetContent() => _builder?.ToString();

    public void AppendLiteral(string s) => _builder?.Append(s);
    public void AppendFormatted<T>(T t) => _builder?.Append(t);
}

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions