Skip to content

C#: new Foo(...) produces no edge, so message publishers and locally built types look unused #2997

Description

@durmazoguzhan

A type that a C# method only constructs gets no edge. new Foo(...) parses as object_creation_expression, and the C# config lists only invocation_expression in call_types, so the construction site is never dispatched in walk_calls.

Java has treated new Foo(...) as a call since #1373, and TypeScript got new_expression extraction in #708. C# never caught up.

Which positions currently emit an edge

I probed one class with the same type used in seven positions (graphify 0.9.48):

public class AsParameter { }
public class AsField { }
public class AsReturn { }
public class AsProperty { }
public class AsLocalDeclared { }
public class AsNewInVar { }
public class AsNewInArg { }

public class Probe
{
    private AsField _field;
    public AsProperty Prop { get; set; }

    public AsReturn Get() => null;
    public void Take(AsParameter p) { }

    public void Body()
    {
        AsLocalDeclared a = null;
        var b = new AsNewInVar();
        Consume(new AsNewInArg());
    }

    private void Consume(object o) { }
}
position edge
parameter type .Take() references AsParameter
field type Probe references AsField
return type .Get() references AsReturn
property type Probe references AsProperty
local declaration none
var x = new Foo() none
Consume(new Foo()) none

The last three classes are in the graph as nodes. Nothing points at them.

The same code in Java

class Worker { void run() { } }
public class Caller {
    public void go() {
        Worker w = new Worker();
        w.run();
    }
}
.go() -> Worker  [calls]  EXTRACTED
.go() -> .run()  [calls]  INFERRED

The C# translation of that file produces only the second edge.

Where it hurts

I ran graphify over a private .NET 8 codebase that moves work between services over MassTransit and RabbitMQ. Producers and consumers do not reference each other, so the only thing that can join them in the graph is the message class: the consumer implements IConsumer<T> and the producer hands a T to the bus.

That works when the producer takes the message as a parameter. Two of the three hops in one chain resolved that way, and both endpoints landed on the same message node. The third hop publishes like this:

var events = models.Select(m => new ProductUpsertRequested { Id = m.Id });
await endpoint.Send(new ProductUpsertRequested { Id = id });

The message type never appears in a declared position there, so the producer side of that node had only a contains edge from its own file. The consumer sat in another service with no path to it. A graphify path between the two ends of the chain returns nothing, and the hop looks like dead code.

The pattern is not specific to a bus. Anything a method builds and hands off has the same shape: a request object passed to a client, a config object passed to a factory, a DTO built for a mapper.

The type is already resolved

This is not a request for new inference. The extractor already types locals well enough to resolve member calls on them:

public class Worker { public void Run() { } }
public class Caller {
    public void Go()
    {
        Worker w = new Worker();
        w.Run();
    }
}
.Go() -> .Run()  [calls]  INFERRED

w is known to be a Worker there. Worker itself is simply never named as a target.

Ambiguity is also already handled. With two classes called Cache in different namespaces, the construction site resolves to neither, which is the behaviour #437 asks for.

Scope

Two things nearby that I would leave alone:

  • Target-typed new() parses as implicit_object_creation_expression and carries no type node. Naming it needs the declared type of the assignment target.
  • A local declared without a constructor (AsLocalDeclared a = null;) still emits nothing. That is a references-from-local-declarations question, separate from this one.

Related

I have a patch with tests and will open a PR against v8.

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