Skip to content

C#: inline-declared receivers (out-var, is/case patterns) are not used as a typing source #2346

Closed

Description

@JensD-git

Summary

Variables declared inline — out T x in an argument, and the pattern forms is T x, is not T x,
case T x: — are not recorded in the receiver type table, so calls on those receivers produce no
calls edge. This is independent of #2299: the behaviour is identical in 0.9.29 and 0.9.31, so the
per-method scoping fix neither caused nor changed it. It surfaced while verifying that fix, because
these receivers used to be resolved accidentally by the old file-wide table leaking a matching type
in from another method.

All four forms have the same shape in the C# grammar — a type node followed by a name node, as
declaration_expression (out-var) or declaration_pattern (the three pattern forms).

Repro

// Types.cs
public class Sect { public bool TryGetEntry(string k) { return true; } }
public class Node { public void Extend(string s) { } }
public class Leaf { public void Touch() { } }
public class Twig { public void Snap() { } }
public class Bag  { public bool TryGetSection(string k, out Sect s) { s = null; return false; } }

// Uses.cs
public class Uses
{
    public void OutVar(Bag bag)
    {
        if (bag.TryGetSection("k", out Sect sect)) { sect.TryGetEntry("x"); }
    }
    public void NegPattern(object o)
    {
        if (o is not Node nd) { return; }
        nd.Extend("c");
    }
    public void PosPattern(object o)
    {
        if (o is Leaf lf) { lf.Touch(); }
    }
    public void CasePattern(object o)
    {
        switch (o) { case Twig tw: tw.Snap(); break; }
    }
}

graphify extract on this directory:

expected calls edge 0.9.29 0.9.31
Uses.OutVar -> Bag.TryGetSection ✅ ✅
Uses.OutVar -> Sect.TryGetEntry — —
Uses.NegPattern -> Node.Extend — —
Uses.PosPattern -> Leaf.Touch — —
Uses.CasePattern -> Twig.Snap — —

1 of 5 edges in both versions. The four missing ones all have their type written explicitly at the
declaration site.

Why it matters

Both idioms are pervasive in modern C#: Try* methods with out parameters, and pattern matching in
if/switch. In our corpus (eight private repositories, ~10.3k .cs files) adding these two forms to
the receiver type table yields +78 additional calls edges with zero edges lost — measured on
identical extraction output, so the delta is attributable to this rule alone.

A representative real example, neutralised:

IniFile iniFile;
lock (IniLock) { iniFile = GetIniFile(path); }

if (iniFile.TryGetSection(pSection, out Section section))
{
    if (section.TryGetEntry(pSetting, out KeyValueEntry entry))  // <- no edge
    {
        builder.Append(entry.Value);
    }
}

iniFile.TryGetSection resolves (0.9.31 handles declare-then-assign), but section.TryGetEntry does
not, even though section is explicitly typed Section two lines above.

Suggested fix

Populate the same per-method type table that #2299 introduced, from two additional node types:

  • declaration_expression — out T x
  • declaration_pattern — is T x, is not T x, case T x:

Both expose a type node and a name node, so one branch covers all four forms. Scoping-wise these behave
like ordinary locals for the remainder of the enclosing block, so no change to the poisoning rules is
needed.

Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions