Jens Geyer created THRIFT-6199:
----------------------------------
Summary: CS0121 for container extension methods shared by programs
that have no include relation
Key: THRIFT-6199
URL: https://issues.apache.org/jira/browse/THRIFT-6199
Project: Thrift
Issue Type: Bug
Components: netstd - Compiler
Reporter: Jens Geyer
h3. Problem Description
Follow-up to THRIFT-6198.
THRIFT-6198 stops the netstd generator from emitting a container extension
method that an *included* program already emits. That covers the reported case,
but the underlying conflict is not tied to the include relation at all: any two
programs that end up in the same C# namespace and use the same container type
declare the same extension method signature, and every call site that sees both
extension classes fails with
{noformat}
error CS0121: The call is ambiguous between the following methods or properties:
'A1Extensions.DeepCopy(List<int>)' and 'A2Extensions.DeepCopy(List<int>)'
{noformat}
Two topologies remain after THRIFT-6198, both verified against master +
THRIFT-6198:
h4. 1. Siblings (the common one)
{code:thrift}
// A1.thrift namespace * MyApp
struct S1 { 1: list<i32> nums }
// A2.thrift namespace * MyApp
struct S2 { 1: list<i32> nums }
// B.thrift namespace * MyApp
include "A1.thrift"
include "A2.thrift"
struct B1 { 1: A1.S1 one, 2: A2.S2 two }
{code}
{{A1Extensions}} and {{A2Extensions}} both declare {{DeepCopy(this
List<int>)}}. Neither program includes the other, so neither generator can see
the conflict. Compiling the {{-r}} output of {{B.thrift}} fails in {{S1.cs}}
and {{S2.cs}}.
h4. 2. Container over base types shared with an otherwise unused include
Program B includes A, uses no type declared in A, and both use
{{list<string>}}. THRIFT-6198 deliberately does not defer here, because B's
generated code would then depend on files it does not otherwise reference.
h3. Why a per-program rule cannot fix this
A generator instance only ever sees its own program and that program's include
closure. In topology 1 neither {{A1}} nor {{A2}} appears in the other's
closure, so no rule evaluated from a single program's point of view can decide
which of the two should emit the method. A whole-run registry (first program in
the run claims the type) was considered and rejected: it makes the content of
{{A1.Extensions.cs}} depend on whether the compiler was invoked with {{-r}} on
the root program or per file, which breaks reproducible output.
h3. Proposed Behaviour
Make the duplicate declarations harmless instead of trying to prevent all of
them. Duplicate extension methods in two static classes are perfectly legal C#;
only an unqualified call that matches both is an error. So generated code
should call a container's {{DeepCopy()}} through the class that owns it rather
than through extension method syntax:
{code:c#}
// now
tmp5.Inners = this.Inners.DeepCopy()!;
// proposed
tmp5.Inners = MyAppTypesExtensions.DeepCopy(this.Inners)!;
{code}
That call site is unambiguous no matter how many extension classes of the
namespace are in scope, and it works the same whether the method stayed with us
or was left to an included program by THRIFT-6198. Struct and union
{{DeepCopy()}} are ordinary instance methods and are unaffected.
Duplicate declarations still remain in the two topologies above, so
hand-written code calling {{someList.DeepCopy()}} in such a namespace continues
to need qualification. Removing the duplicates altogether is out of reach
without the cross-program pass described above.
----
Issue description drafted with AI assistance (Claude Opus 5), reviewed by the
reporter.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)