Skip to content

[http-client-csharp] Dedup operators between generated and customization partials#10639

Merged
jorgerangel-msft merged 4 commits into
mainfrom
copilot/fix-duplicate-operators-in-partials
May 11, 2026
Merged

[http-client-csharp] Dedup operators between generated and customization partials#10639
jorgerangel-msft merged 4 commits into
mainfrom
copilot/fix-duplicate-operators-in-partials

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 11, 2026

Customization partials that declare ==, !=, or implicit/explicit operators on a type whose generated partial also emits those operators (e.g. extensible enums) trigger CS0111 because the dedup path in MethodSignatureBase.SignatureComparer never matches operator methods.

Root cause: the comparer's early Name equality check fails for operators because the two sides use different naming conventions:

Operator Generated (ExtensibleEnumProvider / MrwSerializationTypeDefinition) Customization (NamedTypeSymbolProvider via Roslyn ToDisplayString)
==, != "==", "!=" "operator ==", "operator !="
implicit operator T(...) string.Empty "T" (return type name)
explicit operator T(...) "T" "T"

Only the explicit-operator case happens to align, which is why MRW-serialization dedup tests pass while extensible-enum operators slip through.

Changes

  • MethodSignatureBase.SignatureComparer.Equals — when both signatures carry the Operator modifier:
    • User-defined operators (==, !=, +, …): normalize the name by stripping a leading "operator " prefix before comparing, so "operator ==" matches "==" while == and != remain distinguishable.
    • Conversion operators (implicit/explicit): skip the Name comparison entirely. Modifiers + return type + parameter types fully identify a conversion operator (C# disallows duplicates).
    • Existing return-type and explicit-vs-implicit modifier checks are preserved.
  • MethodSignatureComparerTests — new unit tests covering: == generated/customization name normalization, == vs != not equal, implicit-cast string.Empty matching customization return-type name, and implicit vs explicit not equal.
  • NamedTypeSymbolProviderTests.ValidateOperatorSignaturesMatchGenerated — end-to-end test that uses the existing TestData/Helpers.GetCompilationFromDirectoryAsync() infrastructure to load a WithOperators customization partial, parse its operator signatures via NamedTypeSymbolProvider, and assert they match generated-side signatures (==, !=, implicit operator T(string)) through MethodSignatureBase.SignatureComparer.
  • TypeProviderTests.CanonicalViewDedupesCustomOperators — end-to-end test that builds a generated TypeProvider with ==, !=, and implicit operator MethodProviders, loads a matching customization partial via TestData, and asserts that CanonicalView.Methods contains exactly the three operators all sourced from CustomCodeView (i.e., the generated copies are filtered out).

Example

// Generated partial (from ExtensibleEnumProvider)
public readonly partial struct MyEnum
{
    public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
    public static implicit operator MyEnum(string value) => new MyEnum(value);
}

// Customization partial — previously triggered CS0111 on both operators;
// now correctly suppresses the duplicates from the generated partial.
public readonly partial struct MyEnum : IEquatable<MyEnum>
{
    public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
    public static implicit operator MyEnum(string value) => new MyEnum(value);
}

…ion partials

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/6ebd5fd9-d214-4a9a-bafd-72c4999ed242

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service Bot added the emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp label May 11, 2026
Copilot AI changed the title [WIP] Fix duplicate operators for customization partials [http-client-csharp] Dedup operators between generated and customization partials May 11, 2026
Copilot AI requested a review from jorgerangel-msft May 11, 2026 15:33
Comment thread .chronus/changes/fix-csharp-customization-operator-dedup-2026-5-11-15-23-0.md Outdated
…eSymbolProvider-based test

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/0420672c-19ed-4fa7-bfbe-c1c48af7e741

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-client-csharp@10639

commit: c765004

Copy link
Copy Markdown
Contributor

@jorgerangel-msft jorgerangel-msft left a comment

Choose a reason for hiding this comment

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

@copilot lets also add a test in TypeProviderTests to validate that the resulting canonicalview of a generated type who was customized with operators only has the custom operators

Agent-Logs-Url: https://github.com/microsoft/typespec/sessions/62f6ef1e-a963-4cfb-8c42-4c8c058a268f

Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 11, 2026

@copilot lets also add a test in TypeProviderTests to validate that the resulting canonicalview of a generated type who was customized with operators only has the custom operators

Added TypeProviderTests.CanonicalViewDedupesCustomOperators in c765004. It builds a TestTypeProvider named CustomOperatorType with generated ==, !=, and implicit operator MethodProviders, loads a customization partial (TestData/TypeProviderTests/CanonicalViewDedupesCustomOperators/CustomOperatorType.cs) that declares the same three operators, and asserts CanonicalView.Methods contains exactly the three operators all sourced from CustomCodeView (i.e., the generated copies are filtered out).

@jorgerangel-msft jorgerangel-msft added this pull request to the merge queue May 11, 2026
Merged via the queue into main with commit 07efc2a May 11, 2026
29 checks passed
@jorgerangel-msft jorgerangel-msft deleted the copilot/fix-duplicate-operators-in-partials branch May 11, 2026 20:30
jorgerangel-msft added a commit to jorgerangel-msft/typespec that referenced this pull request May 13, 2026
…ion partials (microsoft#10639)

Customization partials that declare `==`, `!=`, or `implicit`/`explicit`
operators on a type whose generated partial also emits those operators
(e.g. extensible enums) trigger CS0111 because the dedup path in
`MethodSignatureBase.SignatureComparer` never matches operator methods.

Root cause: the comparer's early `Name` equality check fails for
operators because the two sides use different naming conventions:

| Operator | Generated (`ExtensibleEnumProvider` /
`MrwSerializationTypeDefinition`) | Customization
(`NamedTypeSymbolProvider` via Roslyn `ToDisplayString`) |
| --- | --- | --- |
| `==`, `!=` | `"=="`, `"!="` | `"operator =="`, `"operator !="` |
| `implicit operator T(...)` | `string.Empty` | `"T"` (return type name)
|
| `explicit operator T(...)` | `"T"` | `"T"` |

Only the explicit-operator case happens to align, which is why
MRW-serialization dedup tests pass while extensible-enum operators slip
through.

### Changes

- **`MethodSignatureBase.SignatureComparer.Equals`** — when both
signatures carry the `Operator` modifier:
- User-defined operators (`==`, `!=`, `+`, …): normalize the name by
stripping a leading `"operator "` prefix before comparing, so `"operator
=="` matches `"=="` while `==` and `!=` remain distinguishable.
- Conversion operators (`implicit`/`explicit`): skip the `Name`
comparison entirely. Modifiers + return type + parameter types fully
identify a conversion operator (C# disallows duplicates).
- Existing return-type and explicit-vs-implicit modifier checks are
preserved.
- **`MethodSignatureComparerTests`** — new unit tests covering: `==`
generated/customization name normalization, `==` vs `!=` not equal,
implicit-cast `string.Empty` matching customization return-type name,
and implicit vs explicit not equal.
-
**`NamedTypeSymbolProviderTests.ValidateOperatorSignaturesMatchGenerated`**
— end-to-end test that uses the existing
TestData/`Helpers.GetCompilationFromDirectoryAsync()` infrastructure to
load a `WithOperators` customization partial, parse its operator
signatures via `NamedTypeSymbolProvider`, and assert they match
generated-side signatures (`==`, `!=`, `implicit operator T(string)`)
through `MethodSignatureBase.SignatureComparer`.
- **`TypeProviderTests.CanonicalViewDedupesCustomOperators`** —
end-to-end test that builds a generated `TypeProvider` with `==`, `!=`,
and `implicit` operator `MethodProvider`s, loads a matching
customization partial via TestData, and asserts that
`CanonicalView.Methods` contains exactly the three operators all sourced
from `CustomCodeView` (i.e., the generated copies are filtered out).

### Example

```csharp
// Generated partial (from ExtensibleEnumProvider)
public readonly partial struct MyEnum
{
    public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
    public static implicit operator MyEnum(string value) => new MyEnum(value);
}

// Customization partial — previously triggered CS0111 on both operators;
// now correctly suppresses the duplicates from the generated partial.
public readonly partial struct MyEnum : IEquatable<MyEnum>
{
    public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
    public static implicit operator MyEnum(string value) => new MyEnum(value);
}
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
jorgerangel-msft added a commit to jorgerangel-msft/typespec that referenced this pull request May 13, 2026
…ion partials (microsoft#10639)

Customization partials that declare `==`, `!=`, or `implicit`/`explicit`
operators on a type whose generated partial also emits those operators
(e.g. extensible enums) trigger CS0111 because the dedup path in
`MethodSignatureBase.SignatureComparer` never matches operator methods.

Root cause: the comparer's early `Name` equality check fails for
operators because the two sides use different naming conventions:

| Operator | Generated (`ExtensibleEnumProvider` /
`MrwSerializationTypeDefinition`) | Customization
(`NamedTypeSymbolProvider` via Roslyn `ToDisplayString`) |
| --- | --- | --- |
| `==`, `!=` | `"=="`, `"!="` | `"operator =="`, `"operator !="` |
| `implicit operator T(...)` | `string.Empty` | `"T"` (return type name)
|
| `explicit operator T(...)` | `"T"` | `"T"` |

Only the explicit-operator case happens to align, which is why
MRW-serialization dedup tests pass while extensible-enum operators slip
through.

- **`MethodSignatureBase.SignatureComparer.Equals`** — when both
signatures carry the `Operator` modifier:
- User-defined operators (`==`, `!=`, `+`, …): normalize the name by
stripping a leading `"operator "` prefix before comparing, so `"operator
=="` matches `"=="` while `==` and `!=` remain distinguishable.
- Conversion operators (`implicit`/`explicit`): skip the `Name`
comparison entirely. Modifiers + return type + parameter types fully
identify a conversion operator (C# disallows duplicates).
- Existing return-type and explicit-vs-implicit modifier checks are
preserved.
- **`MethodSignatureComparerTests`** — new unit tests covering: `==`
generated/customization name normalization, `==` vs `!=` not equal,
implicit-cast `string.Empty` matching customization return-type name,
and implicit vs explicit not equal.
-
**`NamedTypeSymbolProviderTests.ValidateOperatorSignaturesMatchGenerated`**
— end-to-end test that uses the existing
TestData/`Helpers.GetCompilationFromDirectoryAsync()` infrastructure to
load a `WithOperators` customization partial, parse its operator
signatures via `NamedTypeSymbolProvider`, and assert they match
generated-side signatures (`==`, `!=`, `implicit operator T(string)`)
through `MethodSignatureBase.SignatureComparer`.
- **`TypeProviderTests.CanonicalViewDedupesCustomOperators`** —
end-to-end test that builds a generated `TypeProvider` with `==`, `!=`,
and `implicit` operator `MethodProvider`s, loads a matching
customization partial via TestData, and asserts that
`CanonicalView.Methods` contains exactly the three operators all sourced
from `CustomCodeView` (i.e., the generated copies are filtered out).

```csharp
// Generated partial (from ExtensibleEnumProvider)
public readonly partial struct MyEnum
{
    public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
    public static implicit operator MyEnum(string value) => new MyEnum(value);
}

// Customization partial — previously triggered CS0111 on both operators;
// now correctly suppresses the duplicates from the generated partial.
public readonly partial struct MyEnum : IEquatable<MyEnum>
{
    public static bool operator ==(MyEnum left, MyEnum right) => left.Equals(right);
    public static implicit operator MyEnum(string value) => new MyEnum(value);
}
```

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jorgerangel-msft <102122018+jorgerangel-msft@users.noreply.github.com>
github-merge-queue Bot pushed a commit that referenced this pull request May 15, 2026
… as distinct in conversion-operator dedup (#10706)

Fixes #10705

The operator-dedup path added in #10639 uses `CSharpType.AreNamesEqual`
to compare conversion-operator return types. `AreNamesEqual` only
compares the type `Name`/`FullyQualifiedName` and the generic
`Arguments` list — it never reads `IsNullable`. Because the `CSharpType`
constructor explicitly unwraps `Nullable<T>` for value types and stores
nullability on a separate flag, `T` and `T?` produce identical results
from `AreNamesEqual`.

The practical effect (seen in azure-sdk-for-net PR
[#59283](Azure/azure-sdk-for-net#59283)): a
customization partial that declares `implicit operator T(string)` causes
the dedup comparer to drop **both** the matching generated operator
**and** the generated `implicit operator T?(string)`. When those were
the only members in the generated partial, the file is emitted empty and
the writer drops it — silently removing the nullable conversion operator
from the SDK's public surface.

### Change

In `MethodSignatureBaseEqualityComparer.Equals` (operator branch)
compare `ReturnType.IsNullable` alongside `AreNamesEqual`. Conversion
operators returning `T` and `T?` are now correctly treated as distinct.

### Tests

Added
`ImplicitConversionOperators_NullableAndNonNullableReturnTypes_AreNotEqual`
to `MethodSignatureComparerTests`. All 290 tests in the affected suites
pass.

Co-authored-by: Josh Love <joshlove@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[http-client-csharp] Operators in customization partials are not deduped against generated partial (CS0111)

3 participants